Is React Context a Substitute for Redux?

tl;dr

The consensus seems to be that React Context is ideal for passing state through multiple component levels. Redux obviously handles this well, but it’s like lighting a candle with a flamethrower; if all you’re trying to do is pass state to nested components, then Redux is an unnecessarily powerful tool. However, there are scenarios in which Redux may prove more appropriate than Context.

Application State

When building a React application, managing the state of your app can become tricky as it grows larger and more complex. One way to share information between components is to pass props from a parent component to its child(ren). This is simple enough, but can potentially cause code maintainability and readability problems if your component tree exceeds four or five levels.

Another potential solution is to create many stateful components, each holding the information necessary for that specific component. This can again cause maintainability problems because your app no longer contains a single source of truth. For example, suppose you have two components that use the same piece of information. Updating that information in one component while forgetting to update the other could cause conflicts and inconsistencies within your app.

I’m going to provide a brief introduction to React’s Context API, which can be used to solve these problems, and then talk about some of the limitations with Context.

React Context

React Context provides a way of sharing information with only those components that need it, without explicitly passing props through every level of the component tree. You start by defining a new context:

import React from 'react';
const Context = React.createContext();
// Or using destructuring:
const { Provider, Consumer } = React.createContext();

Context gives you two methods, Provider and Consumer. The Provider should wrap the root of all of the components that need the information (this does not necessarily have to be the root of your app). Before we look at the code, it might be helpful to diagram how this works.

Hypothetical component tree

Here, the Provider is able to pass information straight to the Consumers (i.e. Grandchild 2, Grandchild 3, and Great-Grandchild 1). Child 1, Child 2, and Grandchild 1 are not passing props to the Consumers; in fact they don’t even need to know about this information. Now we’ll look at the code to see how Consumers can access this information.

App.js

MessagesContext.Provider is a JSX tag that takes a prop (which must always be called value) representing the information to be passed to the Consumer. You can also specify a default value when you define a Context. For instance, MessagesContext on line 4 could have been initialized like this:

const MessagesContext = React.createContext('Hello');

That way, if you forget to specify a value prop, or you are providing static information, the Consumer will receive this default value.

Now that we’ve defined the Provider, let’s look deeper into the app to see how components can access the Provider’s information.

UserProfile.js

Notice that UserProfile.js does not need to receive props from its parent, App.js, nor does it pass props to Messages on line 9. Messages.js will grab the value from the Provider directly.

Messages.js

By importing MessagesContext from App.js and rendering its Consumer method, Messages.js is able to display each message in a <p> tag. A few things to note: first, the Consumer requires a function as a child, and second, the argument for this function can be called whatever you like. In App.js, the Provider’s prop is called value, but here in line 7 of Messages.js, we call it messages for clarity.

Drawbacks

Those are the basics of how to use Context and its Provider and Consumer methods. In short, Context works great in “read-only” situations when you only need to pass props down through the component tree. However, there are limitations if you’re trying to allow a Consumer to “write” to the Provider’s state data.

Chief among these, in my opinion, is the difficulty with making API calls. With Context, the call has to be made by the Provider and then passed down. Because Context values and methods need to be wrapped by a Consumer tag to be accessible, there is not an easy way to fetch information in a child component and set it on a parent’s state.

For example, this code will not work:

Provider.js

Consumer.js

MessagesContext's Consumer method cannot be accessed outside of a JSX tag, and therefore cannot be called in a componentDidMount. In this situation, you will have to make the Axios call in the Provider and pass the values down to the Consumer. I prefer not to use this pattern because it could result in a lot of unnecessary information being fetched when you initially load your application.

In this case, it seems simpler to use Redux. Redux allows you to make API calls in the appropriate components and update the global state, or store, only when it becomes necessary to hold the information. There is a learning curve with Redux, but in terms of state management, it helps you run a leaner application. And as Mark Erikson notes in this article, there are a lot of useful features like Redux DevTools and built-in middleware that React Context doesn’t offer.

Summary

Both React Context and Redux have their appropriate use cases and limitations, which creator Dan Abromov has noted all along. For simple state management involving the passing of props through the component tree, Context might be your best bet. But for more complex state management, Redux is more suited for the job. In reality — especially considering that React-Redux utilizes the Context API — the most practical option may be a combination of Context and Redux.

Additional Reading