Review
In part 1 of this post, we went over the fundamentals of state and props in React by building a basic blogging post app. If you are familiar with React basics, feel free to skip the last one and just go through this post.
If you haven’t, you will want to clone the repo, and checkout to the branch 2-edit-form-with-props
. This branch brings us to the point that we ended with in the last post.
Props, pt.2 – More modularization
We go forward with brief modularization. To recap, we have an App component with Post and EditForm subcomponents. We further break down the Post into PostTitle
and PostContent
components (left in the same file for convenience).
// ./src/Post.js
import React from 'react';
const Post = (props) => (
<div className="post">
<PostTitle title={props.title} />
<PostContent content={props.content} />
<button
className="button"
onClick={props.addUpVote}>
+ {props.upvotes}
</button>
</div>
);
export default Post;
const PostTitle = (props) => (
<header className="post__header">
<h1 className="post__title">{props.title}</h1>
</header>
);
const PostContent = (props) => (
<p className="post__body">
{props.content}
</p>
);
But wait!, I hear you say. Isn’t this just unnecessary over-optimization? I won’t argue the point, though of course in a production application with more complex concerns, even components like this may end up containing more in-depth logic that necessitates this level of modularization.
Either way, this goes to illustrate an important point. It is annoying to pass props down to our Post component, to then have to pass it down further to our PostTitle and PostContent components. Kent Dodds refers to this as prop drilling.
This is about the point when many over-eager developers will jump to a tool like Redux to handle global state concerns. But this post is about a new feature in React! And I think we can do better than Redux for such a simple use case … ? …
Enter Context
If only we could have global state that any component could read from. This is exactly what React Context provides!
Instead of resorting to prop drilling, we can hold app-wide state in a “provider.” This process also generates a “consumer,” which can then access these state items as props no matter where the component lives in the app.
Providers and consumers are exactly they sound like. Providers provide the ability for their state to be accessed from any of its children components. Consumers consume the provider’s state as props wherever they are declared.
There are a few different ways to use context and different patterns are emerging, but one pretty clean way is to use the render prop pattern. A render prop is a prop within a component that is a function that will return a component, which can then make use of the arguments passed in that function.
This might seem a bit confusing at first, but the best way to understand is to dive right in.
Building a Context State Holder
We create a new file, app-context.js
that we use to initialize the app-wide state and export React context based on that state.
// ./src/app-context.js
import React from 'react';
export const context = {
title: 'My Very First Blog Post',
content: 'Vim ex mucius tincidunt, at quo justo ceteros facilisis, te erat offendit cum.' +
'Errem oportere cu nam. An tale modus omittantur per, sed fierent detracto ne.' +
'Semper inermis reprimique an mei, qui at probo illum accumsan.' +
'Id quo quod tincidunt scriptorem, et solet prodesset sea.' +
'Sea an ullum similique interesset.',
savePost: () => {},
upvotes: 0,
addUpVote: () => {},
};
export const AppContext = React.createContext(context);
Not so complicated right? The object context
initializes the state, which looks very much like the state we initially created in App.js
in prior iterations.
One thing that might seem a bit odd is our savePost()
and addUpVote()
functions, which do nothing. The reason we add them here is merely as a convenience to let us know that these will be actions we can perform on our state. This functionality will still be performed in App.js
.
Finally, we export an instance of context using React.createContext()
, passing in our context object as the only argment. This is what creates a provider and consumer.
Using Context Provider
In our root component we can now initialize our App’s state using the context
object, import AppContext
, and direct the component to be a provider for our context.
// ./src/App.js
import React, { Component } from 'react';
import './App.css';
import Post from './Post';
import EditForm from './EditForm';
import { AppContext, context } from './app-context';
class App extends Component {
addUpVote = () => {
this.setState({
upvotes: this.state.upvotes + 1,
});
}
savePost = (post) => {
const {
title,
content,
} = post;
this.setState({
title,
content,
})
}
state = {
title: context.title,
content: context.content,
savePost: this.savePost,
upvotes: context.upvotes,
addUpVote: this.addUpVote,
}
render() {
return (
<AppContext.Provider value={this.state}>
<div className="main">
<Post />
<EditForm />
</div>
</AppContext.Provider>
);
}
}
export default App;
Our App component is now more a container for our state and actions than anything else. Notice how we use our context object for our state items, but that our methods to alter the state are still being handled in App.js
directly.
Perhaps it might be a bit confusing to understand how this creates a glue that the rest of our App can read from. And admittedly this is all still new to me as well ?.
What is important here is the value
prop that we pass into our AppContext.Provider
. Whatever is consuming AppContext
will always read from the most updated version of whatever is there (in this case, this.state
).
Anyway, this change will break the app as we’re no longer passing down props directly to our Post and EditForm components. We need to add consumers for our state.
Using Context Consumer
Using the handy render props style previously mentioned, we update Post.js
to use AppContext
‘s consumer.
// ./src/Post.js
import React from 'react';
import { AppContext } from './app-context';
const Post = () => (
<AppContext.Consumer>
{({ title, content, upvotes, addUpVote }) =>
<div className="post">
<PostTitle title={title} />
<PostContent content={content} />
<button
className="button"
onClick={addUpVote}>
+ {upvotes}
</button>
</div>}
</AppContext.Consumer>
);
export default Post;
const PostTitle = (props) => (
<header className="post__header">
<h1 className="post__title">{props.title}</h1>
</header>
);
const PostContent = (props) => (
<p className="post__body">
{props.content}
</p>
);
AppContext.Consumer
is a wrapper component that passes the value object from AppContext.Provider
(in this case the local state from our App component) as a prop. We destructure this prop in the Post component to pass these values as props to PostTitle
and PostContent
. Neat!
Note that context gives us a lot of flexibility in how we want to make use of it. We could just as easily used AppContext.Consumer
once each directly in PostContent
and PostTitle
to completely avoid the need to even pass props down from Post
. Meaning, that the code below would work just the same.
// ./src/Post.js
import React from 'react';
import { AppContext } from './app-context';
const Post = () => (
<AppContext.Consumer>
{({ title, content, upvotes, addUpVote }) =>
<div className="post">
<PostTitle />
<PostContent />
<button
className="button"
onClick={addUpVote}>
+ {upvotes}
</button>
</div>}
</AppContext.Consumer>
);
export default Post;
const PostTitle = () => (
<AppContext.Consumer>
{({ title }) =>
<header className="post__header">
<h1 className="post__title">{title}</h1>
</header>
}
</AppContext.Consumer>
);
const PostContent = () => (
<AppContext.Consumer>
{({ content }) =>
<p className="post__body">
{content}
</p>
}
</AppContext.Consumer>
);
Though this example may seem contrived and cluttered, as an app increases in size and complexity you can see how this would be useful.
And as you can see, there is no limit as to how many times AppContext.Consumer
can be used. As long as it has AppContext.Provider
as a parent somewhere in the tree, we’re good to go. ?
Using Context Consumer in EditForm
Using the same concepts above. We also refactor EditForm.js
.
// ./src/EditForm.js
import React, { Component } from 'react';
import { AppContext, context } from './app-context';
class EditForm extends Component {
state = {
title: context.title,
content: context.content,
}
handleInputChange = (event) => {
const {
value,
name,
} = event.target;
this.setState({
[name]: value
});
}
render() {
const {
title: titleDirty,
content: contentDirty,
} = this.state;
return (
<AppContext.Consumer>
{({ title, content, savePost }) => (
<div className="edit-post">
<label>Title:</label>
<input
className="edit-post__input"
type="text"
name="title"
label="Title"
defaultValue={title}
onChange={this.handleInputChange}
/>
<label>Content:</label>
<textarea
className="edit-post__content"
type="text"
name="content"
defaultValue={content}
onChange={this.handleInputChange}
/>
<button
className="button"
onClick={() => {
savePost({ title: titleDirty, content: contentDirty })
}}>
Save Post
</button>
</div>
)}
</AppContext.Consumer>
)
}
}
export default EditForm;
There is nothing too crazy here. It’s useful to note again that we are still using local state in EditForm. This is just as it should be. The local altered state is a proper local concern to EditForm. Abstracting out the common state makes this clearer and the whole code cleaner in general.
Conclusion
I hope this was a helpful introduction to context for beginner and advanced React user alike. There is a lot to explore with context, and you can probably see the potential for all kinds of amazing patterns to come out of this.
Recent Comments