Flux to Redux: Convert Container Components

In the previous articles we discussed converting Actions and Stores into Reducers.

In this article, we'll discuss converting a Flux Container component into a Redux Container component. Container components act as the glue between your stores/reducers and the underlying "dumb" components.

Converting Container Components

Lets start by taking a look at the Flux Container component...

import React from 'react';  
import UserStore from '../stores/user-store';  
import UserActions from '../actions/user-actions';  
import UserList from '../components/users/user-list.jsx';

export default React.createClass({

  // register the component with the store
  // and loads the users
  componentDidMount() {
    UserStore.register(this.onStoreChanged);
    UserActions.getUsers({});
  },

  // unregister the component with the store
  componentWillUnmount() {
    UserStore.unregister(this.onStoreChanged);
  },

  // use the store's state as the component
  // initial state
  getInitialState() {
    return {
      users: UserStore.getUsers(),
      paging: UserStore.getPaging()
    };
  },

  // this updates the components state when
  // the store state changes
  onStoreChanged() {
    this.setState({ 
      users: UserStore.getUsers(),
      paging: UserStore.getPaging()
    });
  },

  // render the dumb component with the
  // state and adapter to the action methods
  render() {
    return (
      <UserList
        {...this.state}
        pageChanged={this.pageChanged.bind(this)} />
    );   
  },

  // creates an adapater that converts the output from
  // a sub-component call into an action that is dispatched
  pageChanged(page) {
    UserActions.getUsers({ page });
  }

});

The Flux Container component contains a fair amount of boilerplate. In general, Flux Containers listen to stores and update the component state based on the store state.

For starters, the first thing that we do is attach the component to the store. This is done in the componentDidMount method by registering a callback with the store. The callback is the onStoreChanged method on the component.

When fired, onStoreChanged will retrieve state information from the store via its data selector methods getUsers and getPaging. This information will be used by the component's setState method to update the state of the component and trigger a re-render.

There are two other boilerplate methods, componentWillUnmount and getInitialState. The former will unregister the component with the store when the component is removed from the UI. The later is used to load the initial state of the component from the store.

The last two methods are render, which renders the component by supplying the current component state, and the pageChanged method. It is supplied to the sub-component and is an adapter between the sub-component and and the action.

Now lets compare this to the Redux version...

import React, {Component, PropTypes} from 'react';  
import {connect} from 'redux';  
import UserActions from '../actions/user-actions';  
import UserList from '../components/users/user-list.jsx';

class UserListContainer extends Component {

  // validate that mapStateToProps values are correctly
  // supplied by making users and paging required props
  static propTypes = {
    users: PropTypes.array.isRequired,
    paging: PropTypes.object.isRequired,
    getUsers: PropTypes.func.isRequired
  };

  // loads the users on page load
  componentDidMount() {
    this.props.getUsers({});
  }

  // render our dumb component with the props and the
  // adapter function that maps to an action
  render() {
    return (
      <UserList
        {...this.props}
        pageChanged={this.pageChanged.bind(this)} />
    );
  }

  // creates an adapater that converts the output from
  // a sub-component call into an action that is dispatched
  pageChanged(page) {
    this.props.getUsers({ page });
  }
}

const mapStateToProps = (state) => {  
  return {
    users: state.userList.users,
    paging: state.userList.paging
  };
};

const mapDispatchToProps = {  
  getUsers: UserActions.getUsers
};

export default connect(mapStateToProps, mapDispatchToProps)(UserListContainer);  

Converting the Flux container component into a Redux container component removes some boilerplate on the component itself and wires-up the component using the connect method from react-redux.

First thing we'll do to convert the component is create the static propTypes object. This object will validate that our container receives values for required props. In this case, we ensure that users array, paging object, and the getUsers method are supplied.

  static propTypes = {
    users: PropTypes.array.isRequired,
    paging: PropTypes.object.isRequired,
    getUsers: PropTypes.func.isRequired
  };

Next we are still going to retain our componentDidMount method, though we will no longer register it with a store. We will change the loading mechanism from directly invoking the action to using the prop version of the action method.

  // flux
  componentDidMount() {
    UserStore.register(this.onStoreChanged);
    UserActions.getUsers({});
  },
  // redux
  componentDidMount() {
    this.props.getUsers({});
  }

We use the prop version of this method because it is automatically wired into the dispatch method for the Redux store. It is functionally equivalent to doing this.context.store.dispatch(UserActions.getUsers({}). We get this automatic wiring from using the mapDispatchToProps object defined below our component definition. mapDispatchToProps allows us to specify action methods that will be included as props with connect.

Our render method is very similar to the Flux render method. The major difference is that we supplied props instead of state.

  // flux
  render() {
    return (
      <UserList
        {...this.state}
        pageChanged={this.pageChanged.bind(this)} />
    );   
  },
  // redux
  render() {
    return (
      <UserList
        {...this.props}
        pageChanged={this.pageChanged.bind(this)} />
    );
  }

So why do we use props? Well, the connect method of react-redux converts the Redux store's internal state into props. Functionally, this is done in the mapStateToProps method defined below our component. If you look at mapStateToProps you'll see that it is used to create an object from the redux state tree. The properties of the mapStateToProps correspond to props the component receives. In this case, users and paging.

With mapStateToProps and mapDispatchToProps supplied as arguments to connect we have defined our three require props: users, paging, and getUsers.

Lastly, to convert the adapter method pageChanged instead of directly invoking the action creator, we need to use the version supplied as a prop. This is similar to what we did in the componentDidMount method.

  // flux
  pageChanged(page) {
    UserActions.getUsers({ page });
  }
  // redux
  pageChanged(page) {
    this.props.getUsers({ page });
  }

You now have a fully Redux integrated component.

With actions, reducers, and container components written, the dumb components should require zero changes to integrate with the rest of your appliction. Happy coding!

comments powered by Disqus