Why I Avoid useContext To Handle My Global State In React
The dynamic global state of your app can cause unexpected issues and make components unpredictable.
At work, a co-worker was working on implementing a preloading feature for an analytical visual. The side effect performed updated the app's global state once the preloading succeeded.
The problem was when the app’s global state was updated, the whole app re-rendered. This caused the analytical visual and other components to have a glitch-type behavior. This was not sufficient.
The current implementation of the application used the useContext hook to handle our global state.
useContext is a react hook that provides a way to pass data through the component tree without manually passing props down through each nested component.
First / Basic Approach To Implement useContext
My first approach to implementing useContext into an app looked something like this.
The issue with this configuration is when you click the “update state” button, the App’s state gets updated and the components get re-rendered thus re-rendering the Header and Body component.
This re-rendering issue has nothing to do with useContext, but the way useContext is configured.
This configuration would work in situations where the state in useContext is static or when you as a developer don’t mind the issue with the re-rendering of components.
More Efficient Approach To Implement useContext
Instead of explicitly adding the components directly to the child of the <NumberContext.Provider>
, you create a separate component for the Provider and pass in the components as props to render as children.
This approach is preferable if your global state is more dynamic. It solves the issue of all the child components being re-rendered despite subscribing to the context. Now, only the child components subscribed to the context will re-render.
Nested Contexts
But an issue arises when you have a global state that consists of multiple dispatched actions that update different states. Right now, a child component subscribed to the context will re-render regardless of the state being retrieved.
A valid solution to this is to have multiple nested contexts.
With multiple nested contexts, you can differentiate actions and states to separate the subscription to a context that will patch the issue with re-rendering when a state irrelevant to the component receives an update.
Unsolved UseContext Re-render Issue
But there is still a re-rendering issue if a component is subscribed to the context and only retrieves the action. Take the Button component as an example.
const Button = () => {
console.log("render button");
const { setNum } = useContext(NumberContext);
const handleClick = () => setNum(Math.random());
return <button onClick={handleClick}>Update Number Context</button>;
};
When the button is clicked, the NumberContext num
state gets updated, but what you see here is that this Button component will get re-rendered.
It’s not a big deal in this example, but you can probably see an issue for larger and more complex components.
UseContext Becomes Unscalable
Nested contexts can be a solution for smaller applications, but as an app becomes more complex and larger, the issue of multiple contexts can make the app difficult to maintain and possibly see performance issues.
Redux & Redux ToolKit
The purpose of Redux is to be used as a state management library. The benefit of Redux is that there is more control in regards to what components have access to the global state and specified what components get re-rendered.
In the previous example, instead of wrapping the child components with a useContext.Provider, you can remove the use of useContext usage, implement React-Redux, and then only pass the color prop to the necessary components.
You will also need Redux ToolKit which help addresses the issues with Redux store complexity, and boilerplate code.
But the initial set up of redux in an application will look something like this.
But, please visit React-Redux docs for a more extensive tutorial on how to implement Redux into your application.
Now that we have the foundation of redux implemented in the application, we can import useDispatch()
hook and dispatch()
an action to update the global state num
.
And any component in the application can simply access the global color num
by using the useSelector()
hook provided by react-redux to access the global state.
Now when the global state of color is dynamically updated, the components that explicitly use the global state num
will be re-rendered.
You can dispatch from any component within the application, thus making it easier to dynamically update and fetch your global state.
Thunks and Asynchronous Actions
If you are in need to update your global state asynchronously, redux-thunk is a helpful tool to accomplish this. It separates the additional redux-related logic from the UI layer.
Performing logic outside of the UI layer can improve UX by performing updates in the background while the user can still interact with your application.
It’s recommended to avoid a user waiting for an extensive operation to complete. It’s just a bad user experience.
useContext still has its purpose
useContext can be extremely helpful if your global state is static in theory. It provides the convenience of easily giving components access to the global state instead of passing props individual down the component tree.
If your application is small enough where the re-rendering is not a significant issue, I would say go ahead and use useContext. I can see the ease of implementation and understanding in comparison to redux.
Final Thoughts
In comparison to the re-rendering and scalability issue with useContext hook, redux makes your app more predictable and more scalable.
I recommend using other global state management strategies other than useContext. Redux is just one example, but there are alternative options you can choose from.
A few alternative library solutions to handle your global state to consider are:
The reason to navigate away from Redux would be the complexity. Redux has received a lot of criticism because of the related boilerplate. Writing actions and multiple reducers can lead to more code and become hard to maintain. But that’s why Redux Toolkit was created.
Ensure to do intensive research before choosing a library to manage your global state. Things to consider before choosing your global state management library are; the complexity of your app, asynchronous actions, dynamic state of the global state, size, and performance.