My Frustrations With the Context API in React
State management in React has come a long way. Redux is the longtime industry standard, but after the introduction of the Context API, many argue that the days of Redux are over.
I have had the privilege to work with both and use them in large projects.
I can understand the advantages that the Context API brings to the table, but I am going with Redux for my next project.
In a way, this is a love letter to Redux. Let’s begin!
Middleware Support
Middleware is an integral part of my workflow. I use them to show alerts based on the response of remote API calls without ever getting the result back to the component. Some other use cases can be:
Logging
Crash Reporting
Talking to an asynchronous API
Routing
Here is how to do it with Redux:
const middleware = [
...getDefaultMiddleware(),
errorMiddleware,
loggerMiddleware,
];
export const rootStore = configureStore({
reducer: rootReducer,
middleware,
});
These are features that can hardly be ignored in most large-scale projects.
Implementing them with the Context API is possible, but Redux has made it much more manageable. Unfortunately, I find it unnecessarily complex to implement this with Context.
Provider Hell
Whether you’re using the Context API or Redux, it’s advised to keep the size of each reducer/context small for better understanding.
That's not an issue with Redux, as we can use combineReducer()
to combine multiple reducers into a single store and wrap our whole application with that.
But in the Context API, you must wrap your component with the contexts that it actually needs. It creates a provider hell when your application grows large in size.
export const UserComponent = () => {
return (
<Context1 value={value}>
<Context2 value={value2}>
<Context3 value={value3}>
<Context4 value={value4}>
{YOUR_COMPONENT_CODE}
</Context4>
</Context3>
</Context2>
</Context1>
);
}
Yes, I am aware that there are some neat techniques to avoid this issue, like creating an HOC with the required context and wrapping the component with that HOC.
But imagine how many different considerations you have to keep in mind while coding. The situation gets worse when someone else comes and tries to dig through your code.
So why should we make our lives harder?
Familiar Data Flow
Redux has been used by so many projects that there is a high chance that most junior developers have encountered it at some point in their careers.
A typical Redux data flow looks like this: Component -> Actions -> (Middlewares) -> Reducers -> Component.
The workflow and structure of the code are predictable. I think it’s a huge advantage when working on a large project where developers are constantly coming and going.
When using Context, you don’t know where your contexts are. Each developer has their own way of organizing things. This can lead to some unnecessary overhead for others, which is costly.
Performance
You saw this coming. Context has known issues with performance because whenever any part of the context is updated, the whole sub-tree re-renders.
Let’s say we wrap two components with a context. This context holds two values: userDetails
and orderList
.
export const App = () => {
value = {
userDetails: {},
orderList: {}
}
return <Context value={value}>
<UserComponent />
<OrderComponent />
</Context>
}
The UserComponent
depends on the value of userDetails
, whereas OrderComponent
depends on orderList
.
Now if userDetails
updates, both components re-render (although it has nothing to do with OrderComponent
).
Do you see how an extensive application can misuse Context and create huge performance issues?
Yes, there are solutions like splitting the context or memorizing the component before updating. But with the help of selectors, we can avoid this problem altogether. Why reinvent the wheel?
Clear Separation
Redux creates a clear separation of concerns. You can remove almost 100% of your data manipulation from your component.
Most modern applications depend on some kind of remote data anyway. So by using selectors, you can pre-process data before entering into the component.
](https://cdn-images-1.medium.com/max/2656/1*eFg5VVaOjqCKx0EP4VWhUw.png)
It’s a huge win when developing a large application before everybody knows where data comes from and how it’s being modified.
With Context, you don’t have the privilege to do that — or at least it’s tricky to achieve.
Debugging
Regarding debugging, Redux provides an excellent toolkit that makes the debugging experience a breeze.
With the introduction of the redux-toolkit, it has become easier. You turn it on, and the rest is handled by Redux.
With Context, it’s much more challenging to visualize the changes happening. However, you can mitigate the problem by keeping your context small and predictable.
Plugin Support
Redux has rich plugin support that can facilitate many side functionalities that are time-consuming to implement and hard to maintain.
You can think of almost anything and be pleasantly surprised that someone else has already implemented the solution. Some of the most popular ones are:
redux-persist
redux-thunk
redux-form
reselect
These plugins can improve the quality of your application. You have to learn some extra things, but they can reduce the hassle in the long run.
Size
In terms of size, you can argue that Redux needs additional resources to function, increasing the bundle size.
I agree with that, but is it that important when you have the benefits of a rich developer experience and a respectable community?
Final Thoughts
Don’t get me wrong: If you can use Context correctly, most of these problems are solvable. Some might argue that it’s a safer option because it’s built-in.
I agree, but libraries and packages are there for a reason. For smaller projects, the Context API is OK. But for larger ones, I think going with Redux is the safer route.
Let me know what you think in the comments. Have a great day!
Resources
- Redux toolkit: https://redux-toolkit.js.org/
Get in touch with me via LinkedIn