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!