Comparing React Context and Redux
Context limitations when emulating Redux in a small-to-medium size application
With the introduction of Hooks (specifically useReducer
and useContext
) and a stable Context API, it was inevitable that comparisons would be drawn between Context and Redux, perhaps rightly so as most of the technical premises became possible.
This article tries to dig into the technical aspects of Context and its limitations to application performance, maintainability and scalability when emulating Redux architecture using React hooks. It tries to do so based on developer experience when working with a small-to-medium size web application.
This article has the following sections:
- A brief overview of Context API.
- A summary of the application that is used for demonstration purposes.
- Creating the application store using Context with hooks.
- Application performance with Context.
- Application maintainability with Context.
- Conclusion.
1. A Brief Overview of Context API
Context tries to solve the problem of passing down the state to only the components that need it, instead of having to drill it as props. Hence, it makes intermediate components that are not interested in the state ignore it, while making it possible for nested components to access the “global” context state.
To create a context, you use React.createContext
, where you will describe the state that you want to be saved. Then, you create the context provider using MyContext.Provider
at the topmost component that will need to access the context state.
Last, all the components that are descendants of the provider tree can consume the state using:
MyContext.Consumer
oruseContext
hook within functional components.this.context
orstatic contextType = MyContext
within class components.
In our application, we will be using the useContext
hook.
2. A Summary of the Application That Will Be Used for Demonstration Purposes
The application is called Prep&Groc (short for Prepare and Groceries). It’s a web application that allows a user to prepare recipes by finding what recipe ingredients are missing from those that he currently has stored in his fridge. A user can save missing ingredients to a grocery list items where they can mark them as complete while doing the groceries. The application also displays notification messages depending on the user’s action result.

The client-side of the application is implemented with both Context and Redux so that we can better understand and draw a comparison between the two. Here are the repository links for each implementation:
You can also take a look at the live running version of Prep&Groc.
3. Creating the Application Store Using Context With Hooks
useReducer
is a React hook that allows complex application state management. It accepts a reducer function and an initial state as parameters list while returning the current state and a dispatcher function. The dispatcher is used to issue actions that will change the state of reducer.
Following the redux paradigm of a single application global store, we can similarly create it with a Context and useReducer
hook.
We use separate contexts for state and dispatch simply because dispatch function never changes and we want to try and save a few application renders. Then, both StoreUpdateProvider
and StoreProvider
are created in StoreContextProvider
which now provides the global state of the application.
At the application’s Root component we can import and use the StoreContextProvider
as follows:
appReducer
encapsulates the entire application state by making use of reducer functions similar to how combineReducers
does in Redux. It simply splits the application state into reducers, making it easier to manage.
4. Context Performance
The most important bit to understand about Context is that it will re-render all components that are descendants of the context provider, whether or not they consume the context when a state change happens. The only exception to this rule is when the context provider’s children do not change and only the components that consume the context will re-render. This is the main reason we pass the children prop to components that provide a context instead of creating components inside them (Please read James Nelson blog post on avoiding unnecessary Context renders).
The bottom line is that we end up with components that are re-rendered, even though they may have not changed, whereas we would ideally want to re-render only those to which changes did occur. At least that’s the case with Redux when used in accord with library guidelines and practices.
The best strategy to reduce unnecessary re-renders is to split the context into other contexts (or create new ones from scratch). A strong case to do so is when:
- Part of the state changes frequently and we want to isolate it.
- Components are only interested to consume a part of the application state.
- Part of the state changes infrequently and components may consume both the part and the whole state.
To carefully demonstrate the scenarios when we might want to split the context, let’s first describe a common page of our application. We have the fridge page which at the page header has a form that saves the ingredients and a fridge ingredients list as page body. Moreover, when a user successfully deletes a fridge ingredient it displays a notification message saying “Fridge ingredient was successfully deleted.”

We can gain slightly better performance by trying to reduce unnecessary re-renders whenever a notification happens.
Currently, when a user completes an action that displays a notification the entire page is re-rendered twice — once upon the addition of notification on the screen and again on its removal. Splitting the notifications substate from the store context state falls into the scenario of isolating the frequent changes (an overwhelming number of user actions issue a notification) and where the components are interested to consume only the part of the state.
As we did with StoreContextProvider
, we’ll implement the NotificationContextProvider
component.
Our Root component will create all context providers. We have removed the notifications reducer from appReducer
and instead we pass it as a prop value to the newly created NotificationContextProvider
component.
Finally, the NotificationToast
component (which is responsible for managing notifications) will solely consume the notification context state and its dispatcher function, which we named “notify.”
Now, whenever a component will dispatch a notification it will re-render only the NotificationToast
component and not the whole application.

Using React dev profiler to record our action we can see that when the notification happens only the NotificationToast
component is affected (please look at the third and fourth render from the recorded session of profiler). Thus, not only did we improve our application performance (this comes with a maintainability cost which we will see in the next section) but more importantly we’ve re-rendered only the components that have changed (this applies for NotificationToast
component only).
We will only discuss the case when substate changes are infrequent and components may consume state from both the part and the whole application state. It’s becoming increasingly common practice for applications to have a theme where a user selects his application view preference. This part of the state is not supposed to change often. As a result, we can split the theme state and its dispatch function into separate contexts. Components will be interested to consume both the theme context and store context provider but because theme state changes are infrequent, in this case, we will not be bothered with components’ unnecessary re-renders (another similar scenario is user authentication).
Vain Context Splits
Splitting contexts at first seems like a good strategy for reducing unnecessary re-renders in React. However, it is more nuanced and comes with a caveat or a set of limitations.
It’s important that before splitting or creating a new context, one must take into consideration how that context will be consumed from components. If the part state that you have created is still being consumed at the same time with the whole state by the majority of components and frequent changes occur then it is probably a better idea to merge or not split them at all because you will barely have any benefit of application performance. In our application, this case could be if some of the page components also consumed the notification state context, for example.
A Case for Memoization
React library provides a variety of memoization mechanisms that work on different granular levels.
React.memo can be used to memoize functional components as it performs a shallow comparison of component props.
“It’s similar to
React.PureComponent
but for function components instead of classes.” Official React documentation.
React.useMemo
is a hook that can also be used for functional component memoization purposes.
“
useMemo
will only recompute the memoized value when one of the dependencies has changed.” Official React documentation.
So, one might try to use them to improve application performance and save components that have not changed from being re-rendered. Except that when used with Context they will still be re-rendered. Having said that, the main motivation behind memoization of components when using Context should be driven by expensive calculations or expensive component renderings. Do not use memoization techniques to reduce unnecessary components’ re-renders.
5. Context Maintainability
While one might be able to successfully split or create a new context and successfully improve application performance, the problem that arises is that user actions will usually need to change the state of multiple contexts. This means that a specific user action will have to accept as parameters list the dispatch functions for each context that it has to communicate the change. This also means that components which contain those user actions will have to consume multiple contexts. For instance, in our action of deleting a fridge ingredient, after having created the NotificationContextProvider
we have to pass as parameters list:
- The dispatch function of the store context.
- The dispatch function of the notification update (named
notify
).
Although we might improve our application performance, code readability suffers. Now, we have to take care of all needed parameters and pass them to the action, which in turn will do the same for all the actions that it depends on (delete fridge ingredient action uses removePageItem
, addSuccessToast
, and handleFetchError
actions).
On top of that, our components will be crowded with code that consumes the contexts. Although, one can always use custom hooks to alleviate this problem by sharing the common state between components. For instance, we are using the useDispatchActionFromButton
custom hook inside DeleteFridgeIngredientButton
to gather all the necessary dispatch functions so that we can successfully invoke deleteFridgeIngredient
action.
While creating more contexts may benefit your performance, it’s deemed to not be a sustainable solution regarding the maintainability of the application. User actions have to change the state of multiple contexts. In contrast to Context, Redux has a single dispatch function and when actions are asynchronous then you will not need to pass the dispatcher in the parameters list because it is taken care of by Redux middlewares. Redux middlewares such as redux-thunk or sagas normalizes an async action by deferring it until the action has been resolved to a proper action object. A sample of the same deleteFridgeIngredient
action with Redux is shown below:
When using Redux you will need to concern yourself only about the action logic itself while you can still create custom hooks or use the out of the box useDispatch
hook from the Redux library:
The Case for Inexpensive Re-Renders
ReactJS is fast at what it does mostly due to its reliance on Virtual DOM. With the upcoming React concurrent mode, the applications will likely appear to work even faster as it will be able to suspend tasks based on the priority that will improve the interaction time with the user. Even with the current version of React most application re-renders are considered inexpensive as they normally fall short of the time that a user starts to perceive a delay of his action. Based on that assumption, one could argue that performance optimizations aren’t needed, as it’s already acceptable from the user point of view.
Assuming that performance is acceptable and that we’re using a single application store context, the problem is the lack of uniformity towards user asynchronous and synchronous actions. Dispatching an async action looks different from dispatching a synchronous one. In the former case, the action looks more like a helper function.
Let’s have a look at delete fridge ingredient async action that runs when FridgeIngredientButton
is clicked and the sync action that runs when the notification message is closed inside NotificationToast
component. When running deleteFridgeIngredient
async action we have to pass the dispatch function as a parameter instead of dispatching it directly from the component.
As I mentioned before, Redux solves this problem with middleware that lets you write async code and normalize all actions. We are using redux-thunk middleware to handle async code interaction with the store.
The invocation of synchronous actions when using Context remains the same as the Redux actions invocation pattern.
6. Conclusion
Based on the experience that I had when imitating Redux with Context and hooks I make the following conclusions:
- While technically it’s possible to substitute Redux with Context, limitations of application performance and maintainability continue to make it an expensive choice as the application grows in size.
- If you’re unsure whether or not Context will be able to cope with your application requirements, start using it and emulate Redux architecture. When you reach an endpoint you can easily switch to Redux (most of the infrastructure will be in place anyway).
- When optimizing application performance with Context, let the components mature first. Afterward, you can start tweaking the performance of the application by carefully observing what information is being consumed from components and user actions. This will give you a better idea about the possible context candidates.
Context and Redux are not a substitute for each other, neither are they mutually exclusive. Most of the Redux complaints that I have seen are about the boilerplate code that one needs to build. In fact, that’s what happens with any well-defined architectural style because it has a set of principles, constraints or flow of data upon which it imposes itself to successfully work and meet the requirements.
In Plain English 🚀
Thank you for being a part of the In Plain English community! Before you go:
- Be sure to clap and follow the writer ️👏️️
- Follow us: X | LinkedIn | YouTube | Discord | Newsletter
- Visit our other platforms: Stackademic | CoFeed | Venture
- More content at PlainEnglish.io