React 16.3 added a new Context API – new in the sense that the old context API was a behind-the-scenes feature that most people either didn’t know about, or avoided using because the docs said to avoid using it.
Now, though, the Context API is a first-class citizen in React, open to all (not that it wasn’t before, but it’s, like, official now).
As soon as React 16.3 came out there were articles all across the web proclaiming the death of Redux because of this new Context API. If you asked Redux, though, I think it would say “the reports of my death are greatly exaggerated.”
In this post I want to cover how the new Context API works, how it is similar to Redux, when you might want to use Context instead of Redux, and why Context doesn’t replace the need for Redux in every case.
A Motivating Example
I’m going to assume you’ve got the basics of React down pat (props & state), but if you don’t, I have a free 5-day course to help you learn react here.
Let’s look at an example that would cause most people to reach for Redux. We’ll start with a plain React version, and then see what it looks like in Redux, and finally with Context.
This app has the user’s information displayed in two places: in the nav bar at the top-right, and in the sidebar next to the main content.
The component structure looks like this:
With pure React (just regular props), we need to store the user’s info high enough in the tree that it can be passed down to the components that need it. In this case, the keeper of user info has to be App
.
Then, in order to get the user info down to the components that need it, App needs to pass it along to Nav and Body. They, in turn, need to pass it down again, to UserAvatar (hooray!) and Sidebar. Finally, Sidebar has to pass it down to UserStats.
Let’s look at how this works in code (I’m putting everything in one file to make it easier to read, but in reality these would probably be split out into separate files).
import React from "react";
import ReactDOM from "react-dom";
import "./styles.css";
const UserAvatar = ({ user, size }) => (
<img
className={`user-avatar ${size || ""}`}
alt="user avatar"
src={user.avatar}
/>
);
const UserStats = ({ user }) => (
<div className="user-stats">
<div>
<UserAvatar user={user} />
{user.name}
</div>
<div className="stats">
<div>{user.followers} Followers</div>
<div>Following {user.following}</div>
</div>
</div>
);
const Nav = ({ user }) => (
<div className="nav">
<UserAvatar user={user} size="small" />
</div>
);
const Content = () => <div className="content">main content here</div>;
const Sidebar = ({ user }) => (
<div className="sidebar">
<UserStats user={user} />
</div>
);
const Body = ({ user }) => (
<div className="body">
<Sidebar user={user} />
<Content user={user} />
</div>
);
class App extends React.Component {
state = {
user: {
avatar:
"https://www.gravatar.com/avatar/5c3dd2d257ff0e14dbd2583485dbd44b",
name: "Dave",
followers: 1234,
following: 123
}
};
render() {
const { user } = this.state;
return (
<div className="app">
<Nav user={user} />
<Body user={user} />
</div>
);
}
}
ReactDOM.render(<App />, document.querySelector("#root"));
Here’s a working example on CodeSandbox.
Now, this isn’t terrible. It works just fine. But it’s a bit annoying to write. And it gets more annoying when you have to pass down a lot of props (instead of just one).
There’s a bigger downside to this “prop drilling” strategy though: it creates coupling between components that would otherwise be decoupled. In the example above, Nav
needs to accept a “user” prop and pass it down to UserAvatar
, even though Nav does not have any need for the user
otherwise.
Tightly-coupled components (like ones that forward props down to their children) are more difficult to reuse, because you’ve gotta wire them up with their new parents whenever you plop one down in a new location.
Let’s look at how we might improve it with Redux.
Redux Example
I’m going to go through the Redux example quickly so we can look more deeply at how Context works, so if you are fuzzy on Redux, read this intro to Redux first (or watch the video).
Here’s the React app from above, refactored to use Redux. The user
info has been moved to the Redux store, which means we can use react-redux’s connect
function to directly inject the user
prop into components that need it.
This is a big win in terms of decoupling. Take a look at Nav
, Body
, and Sidebar
and you’ll see that they’re no longer accepting and passing dow the user
prop. No more playing hot potato with props. No more needless coupling.
import React from "react";
import ReactDOM from "react-dom";
import { createStore } from "redux";
import { connect, Provider } from "react-redux";
const initialState = {};
function reducer(state = initialState, action) {
switch (action.type) {
case "SET_USER":
return {
...state,
user: action.user
};
<span class="nl">default:
return state;
}
}
const store = createStore(reducer);
store.dispatch({
type: "SET_USER",
user: {
avatar: "https://www.gravatar.com/avatar/5c3dd2d257ff0e14dbd2583485dbd44b",
name: "Dave",
followers: 1234,
following: 123
}
});
const mapStateToProps = state => ({
user: state.user
});
const UserAvatar = connect(mapStateToProps)(({ user, size }) => (
<img
className={`user-avatar ${size || ""}`}
alt="user avatar"
src={user.avatar}
/>
));
const UserStats = connect(mapStateToProps)(({ user }) => (
<div className="user-stats">
<div>
<UserAvatar user={user} />
{user.name}
</div>
<div className="stats">
<div>{user.followers} Followers</div>
<div>Following {user.following}</div>
</div>
</div>
));
const Nav = () => (
<div className="nav">
<UserAvatar size="small" />
</div>
);
const Content = () => (
<div className="content">main content here</div>
);
const Sidebar = () => (
<div className="sidebar">
<UserStats />
</div>
);
const Body = () => (
<div className="body">
<Sidebar />
<Content />
</div>
);
const App = () => (
<div className="app">
<Nav />
<Body />
</div>
);
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.querySelector("#root")
);
Here’s the Redux example on CodeSandbox.
Now you might be wondering how Redux achieves this magic. It’s a good thing to wonder. How is it that React doesn’t support passing props down multiple levels, but Redux is able to do it?
The answer is, Redux uses React’s context feature. Not the modern Context API (not yet) – the old one. The one the React docs said not to use unless you were writing a library or knew what you were doing.
Context is like an electrical bus running behind every component: to receive the power (data) passing through it, you need only plug in. And (React-)Redux’s connect
function does just that.
This feature of Redux is just the tip of the iceberg, though. Passing data around all over the place is just the most apparent of Redux’s features. Here are a few other benefits you get out of the box:
connect
is pure
connect
automatically makes connected components “pure,” meaning they will only re-render when their props change – a.k.a. when their slice of the Redux state changes. This prevents needless re-renders and keeps your app running fast. DIY method: Create a class that extends PureComponent
, or implement shouldComponentUpdate
yourself.
Easy Debugging with Redux
The ceremony of writing actions and reducers is balanced by the awesome debugging power it affords you.
With the Redux DevTools extension you get an automatic log of every action your app performed. At any time you can pop it open and see which actions fired, what their payload was, and the state before and after the action occurred.
Another great feature the Redux DevTools enable is time travel debugging a.k.a. you can click on any past action and jump to that point in time, basically replaying every action up to and including that one (but no further). The reason this can work is because each action immutably update’s the state, so you can take a list of recorded state updates and replay them, with no ill effects, and end up where you expect.
Then there are tools like LogRocket that basically give you an always-on Redux DevTools in production for every one of your users. Got a bug report? Sweet. Look up that user’s session in LogRocket and you can see a replay of what they did, and exactly which actions fired. That all works by tapping into Redux’s stream of actions.
Customize Redux with Middleware
Redux supports the concept of middleware, which is a fancy word for “a function that runs every time an action is dispatched.” Writing your own middleware isn’t as hard as it might seem, and it enables some powerful stuff.
For instance…
- Want to kick off an API request every time an action name starts with
FETCH_
? You could do that with middleware. - Want a centralized place to log events to your analytics software? Middleware is a good place for that.
- Want to prevent certain actions from firing at certain times? You can do that with middleware, transparent to the rest of your app.
- Want to intercept actions that have a JWT token and save them to localStorage, automatically? Yep, middleware.
Here’s a good article with some examples of how to write Redux middleware.
How to Use the React Context API
But hey, maybe you don’t need all those fancy features of Redux. Maybe you don’t care about the easy debugging, the customization, or the automatic performance improvements – all you want to do is pass data around easily. Maybe your app is small, or you just need to get something working and address the fancy stuff later.
React’s new Context API will probably fit the bill. Let’s see how it works.
I published a quick Context API lesson on Egghead if you’d rather watch than read (3:43):
There are 3 important pieces to the context API:
- The
React.createContext
function which creates the context - The
Provider
(returned by createContext
) which establishes the “electrical bus” running through a component tree - The
Consumer
(also returned by createContext
) which taps into the “electrical bus” to extract the data
The Provider
is very similar to React-Redux’s Provider
. It accepts a value
prop which can be whatever you want (it could even be a Redux store… but that’d be silly). It’ll most likely be an object containing your data and any actions you want to be able to perform on the data.
The Consumer
works a little bit like React-Redux’s connect
function, tapping into the data and making it available to the component that uses it.
Here are the highlights:
<code>
const UserContext = React.createContext();
const UserAvatar = ({ size }) => (
<UserContext.Consumer>
{user => (
<img
className={`user-avatar ${size || ""}`}
alt="user avatar"
src={user.avatar}
/>
)}
</UserContext.Consumer>
);
const UserStats = () => (
<UserContext.Consumer>
{user => (
<div className="user-stats">
<div>
<UserAvatar user={user} />
{user.name}
</div>
<div className="stats">
<div>{user.followers} Followers</div>
<div>Following {user.following}</div>
</div>
</div>
)}
</UserContext.Consumer>
);
class App extends React.Component {
state = {
user: {
avatar:
"https://www.gravatar.com/avatar/5c3dd2d257ff0e14dbd2583485dbd44b",
name: "Dave",
followers: 1234,
following: 123
}
};
render() {
return (
<div className="app">
<UserContext.Provider value={this.state.user}>
<Nav />
<Body />
</UserContext.Provider>
</div>
);
}
}
</code>
Here’s the full code in a CodeSandbox.
Let’s go over how this works.
Remember there’s 3 pieces: the context itself (created with React.createContext
), and the two components that talk to it (Provider
and Consumer
).
Provider and Consumer are a Pair
The Provider and Consumer are bound together. Inseperable. And they only know how to talk to each other. If you created two separate contexts, say “Context1” and “Context2”, then Context1’s Provider and Consumer would not be able to communicate with Context2’s Provider and Consumer.
Context Holds No State
Notice how the context does not have its own state. It is merely a conduit for your data. You have to pass a value to the Provider
, and that exact value gets passed down to any Consumer
s that know how to look for it (Consumers that are bound to the same context as the Provider).
When you create the context, you can pass in a “default value” like this:
<code>const Ctx = React.createContext(yourDefaultValue);
</code>
This default value is what the Consumer
will receive when it is placed in a tree with no Provider
above it. If you don’t pass one, the value will just be undefined
. Note, though, that this is a default value, not an initial value. A context doesn’t retain anything; it merely distributes the data you pass in.
Consumer Uses the Render Props Pattern
Redux’s connect
function is a higher-order component (or HoC for short). It wraps another component and passes props into it.
The context Consumer
, by contrast, expects the child component to be a function. It then calls that function at render time, passing in the value that it got from the Provider
somewhere above it (or the context’s default value, or undefined
if you didn’t pass a default).
Provider Accepts One Value
Just a single value, as the value
prop. But remember that the value can be anything. In practice, if you want to pass multiple values down, you’d create an object with all the values and pass that object down.
That’s pretty much the nuts and bolts of the Context API.
Context API is Flexible
Since creating a context gives us two components to work with (Provider and Consumer), we’re free to use them however we want. Here are a couple ideas.
Turn the Consumer into a Higher-Order Component
Not fond of the idea of adding the UserContext.Consumer
around every place that needs it? Well, it’s your code! You can do what you want. You’re an adult.
If you’d rather receive the value as a prop, you could write a little wrapper around the Consumer
like this:
function withUser(Component) {
return function ConnectedComponent(props) {
return (
<UserContext.Consumer>
{user => <Component <span class="err">{...props<span class="err">} user={user}/>}
</UserContext.Consumer>
);
}
}
And then you could rewrite, say, UserAvatar
to use this new withUser
function:
const UserAvatar = withUser(({ size, user }) => (
<img
className={`user-avatar ${size || ""}`}
alt="user avatar"
src={user.avatar}
/>
));
And BOOM, context can work just like Redux’s connect
. Minus the automatic purity.
Here’s an example CodeSandbox with this higher-order component.
Hold State in the Provider
The context’s Provider is just a conduit, remember. It doesn’t retain any data. But that doesn’t stop you from making your own wrapper to hold the data.
In the example above, I left App
holding the data, so that the only new thing you’d need to understand was the Provider + Consumer components. But maybe you want to make your own “store”, of sorts. You could create a component to hold the state and pass them through context:
class UserStore extends React.Component {
state = {
user: {
avatar:
"https://www.gravatar.com/avatar/5c3dd2d257ff0e14dbd2583485dbd44b",
name: "Dave",
followers: 1234,
following: 123
}
};
render() {
return (
<UserContext.Provider value={this.state.user}>
{this.props.children}
</UserContext.Provider>
);
}
}
const App = () => (
<div className="app">
<Nav />
<Body />
</div>
);
ReactDOM.render(
<UserStore>
<App />
</UserStore>,
document.querySelector("#root")
);
Now your user data is nicely contained in its own component whose sole concern is user data. Awesome. App
can be stateless once again. I think it looks a little cleaner, too.
Here’s an example CodeSandbox with this UserStore.
Pass Actions Down Through Context
Rememeber that the object being passed down through the Provider
can contain whatever you want. Which means it can contain functions. You might even call them “actions.”
Here’s a new example: a simple Room with a lightswitch to toggle the background color – err, I mean lights.
The state is kept in the store, which also has a function to toggle the light. Both the state and the function are passed down through context.
import React from "react";
import ReactDOM from "react-dom";
import "./styles.css";
const RoomContext = React.createContext();
class RoomStore extends React.Component {
state = {
isLit: <span class="kc">false
};
toggleLight = () => {
this.setState(state => ({ isLit: !state.isLit }));
};
render() {
return (
<RoomContext.Provider
value={{
isLit: this.state.isLit,
onToggleLight: this.toggleLight
}}
>
{this.props.children}
</RoomContext.Provider>
);
}
}
const Room = () => (
<RoomContext.Consumer>
{({ isLit, onToggleLight }) => (
<div className={`room ${isLit ? "lit" : "dark"}`}>
The room is {isLit ? "lit" : "dark"}.
<br />
<button onClick={onToggleLight}>Flip</button>
</div>
)}
</RoomContext.Consumer>
);
const App = () => (
<div className="app">
<Room />
</div>
);
ReactDOM.render(
<RoomStore>
<App />
</RoomStore>,
document.querySelector("#root")
);
Here’s the full working example in CodeSandbox.
Should You Use Context, or Redux?
Now that you’ve seen both ways – which one should you use? Well, if there’s one thing that will make your apps better and more fun to write, it’s taking control of making the decisions. I know you might just want “The Answer,” but I’m sorry to have to tell you, “it depends.”
It depends on things like how big your app is, or will grow to be. How many people will work on it – just you, or a larger team? How experienced are you or your team with functional concepts (the ones Redux relies upon, like immutability and pure functions).
One big pernicious fallacy that pervades the JavaScript ecosystem is the idea of competition. The idea that every choice is a zero-sum game: if you use Library A, you must not use its competitor Library B. The idea that when a new library comes out that’s better in some way, that it must supplant an existing one. There’s a perception that everything must be either/or, that you must either choose The Best Most Recent or be relegated to the back room with the developers of yesteryear.
A better approach is to look at this wonderful array of choices like a toolbox. It’s like the choice between using a screwdriver or an impact driver. For 80% of the jobs, the impact driver is gonna put the screw in faster than the screwdriver. But for that other 20%, the screwdriver is actually the better choice – maybe because the space is tight, or the item is delicate. When I got an impact driver I didn’t immediately throw away my screwdriver, or even my non-impact drill. The impact driver didn’t replace them, it simply gave me another option. Another way to solve a problem.
Context doesn’t “replace” Redux any more than React “replaced” Angular or jQuery. Heck, I still use jQuery when I need to do something quick. I still sometimes use server-rendered EJS templates instead of spinning up a whole React app. Sometimes React is more than you need for the task at hand. Sometimes Redux is more than you need.
Today, when Redux is more than you need, you can reach for Context.
Redux vs. The React Context API was originally published by Dave Ceddia at Dave Ceddia on July 17, 2018.