While you learned in an earlier article that you should aim to have very few containers, there are exceptions. When you finish this article, you should be able to describe a situation where defining multiple containers for a single component is advantageous.
Separating your concerns with presentational and container components allows you to reuse presentational components where it makes sense, rather than duplicating code. If a presentational component needs different data in each situation, though, you may need more containers. By creating more container components, you can render the same presentational component with each of those containers to suit different needs.
Consider a form component that may either create or edit a post. The form itself looks and works the same in both cases; it has a few inputs and a submit button. The use cases differ, though, in that the edit form needs to map state from the store to its props, while the create form does not. Furthermore, the edit form will need to dispatch a different action when the form submits than the create form will, as well as request the object from our backend.
As you go through the code snippets below, read the comments carefully.
Here’s the presentational component, PostForm
:
// PostForm.js
import React from 'react';
class PostForm extends React.Component {
constructor(props) {
super(props);
// set up initial state
this.state = this.props.post; // a Post object has a title and a body
}
static getDerivedStateFromProps(props, state) {
// if we get a different post in props, we'll need to set state
if (props.post.id !== state.id) {
return props.post;
}
}
update = (field) => {
return (e) => {
this.setState({ [field]: e.target.value });
};
}
handleSubmit = (e) => {
e.preventDefault();
// `submit` will be a thunk action that presumably creates or edits a post
this.props.submit(this.state);
}
render() {
return (
<form onSubmit={this.handleSubmit}>
<label>
Title
<input
type="text"
onChange={this.update("title")}
value={this.state.title}
/>
</label>
<label>
Body
<input
type="text"
onChange={this.update("body")}
value={this.state.body}
/>
</label>
<button>Submit Post</button>
</form>
);
}
}
export default PostForm;
You can see that PostForm
is expecting two things in props: a post
object and a submit
function. The container will have to define these, since right now, this form can’t actually do anything. Give it the ability to create a post:
// CreatePostFormContainer.js
import { connect } from 'react-redux';
import PostForm from './PostForm';
import { createPost } from '../actions/postActions';
const mapStateToProps = state => {
return {
post: { title: '', body: '' } // a default blank object
};
};
const mapDispatchToProps = dispatch => {
return {
submit: post => dispatch(createPost(post))
};
};
export default connect(
mapStateToProps,
mapDispatchToProps
)(PostForm);
So far, this is nothing new. Now, wherever you need a form to create a post, you can render CreatePostFormContainer
by importing from the above file.
But what about editing? This is a little trickier, because you need more information from the store - so you’ll need a higher-order component to help you out. Higher-order components are a useful React pattern that essentially uses a component to render another component, usually to handle some sort of work and pass in data. This pattern allows us to keep your components small and modular. Here, you’ll use a higher-order component to fetch the post you want to edit and pass it into the PostForm
:
// EditPostFormContainer.js
import React from 'react';
import { connect } from 'react-redux';
import PostForm from './PostForm';
import { fetchPost, updatePost } from '../actions/postActions';
import { selectPost } from '../reducers/postSelectors';
const mapStateToProps = (state, ownProps) => {
const defaultPost = { title: '', body: '' };
const post = selectPost(ownProps.match.params.postId) || defaultPost;
// get the post this route is asking for
// (assuming here that this component is being rendered by a route)
// if you don't have the post in state yet, return a blank post so PostForm doesn't break
return { post };
};
const mapDispatchToProps = dispatch => {
// an edit form will need to fetch the relevant post, but the PostForm shouldn't handle that
// you'll handle this problem with a higher-order component, EditPostFormContainer
return {
fetchPost: id => dispatch(fetchPost(id)),
submit: post => dispatch(updatePost(post))
};
};
class EditPostForm extends React.Component {
// this is the higher-order component made to handle the fetch
componentDidMount() {
// do the fetching here so that PostForm doesn't have to
this.props.fetchPost(this.props.match.params.postId);
}
render() {
// destructure the props so you can easily pass them down to PostForm
const { post, submit } = this.props;
return <PostForm post={post} submit={submit} />;
}
}
// now `connect` it to the Redux store
export default connect(
mapStateToProps,
mapDispatchToProps
)(EditPostForm);
The result here is that we can render a CreatePostFormContainer
wherever you want a form to create a post, and an EditPostFormContainer
wherever you want to edit a post. Both components will render a PostForm
, but each will have different functions. The PostForm
also gets to be very simple and make almost no decisions. This helps keep your code DRY and modular.
You can use this pattern with any presentational component that needs to be connected to the store, but may need entirely different data to perform different functions.
In this article, you learned about a situation where defining multiple containers for a single component is advantageous.