Most of us are very used to the global state concept and we have been applying this pattern in our apps extensively. There are plenty of third party libraries aiming to provide a single place to store our state, but when we are talking about React, Redux is the king in that regard. I bet most of you have been in touch with Redux for quite a long time.
With the release of context API in React 16.3 and especially hooks in React 16.8, a new world of possibilites suddenly arose. As we discovered the cleanliness and simplicity of hooks, Redux started to feel like overly complex and verbose with all those containers, reducers, action creators and so on. The question didn't take too long; if new React context was designed to feed a whole component tree with data, wouldn't it be possible to redefine the global state pattern by using native context plus hooks?
In this post, we are going to show you the main issues of this pattern and a few different approaches to achieve global state using React native features.
In this article we assume that you are already familiar with React Hooks, we will make use of useReducer, useCallback, useMemo... amongst others. You can learn more about it in the official hooks api reference
React context suffers from a common problem: useContext
hook will re-render whenever your context is modified. In other words, every component subscribed to our global state will re-render upon a context change, which in some scenarios might lead to performance issues.
In this post we will go through several techniques to mitigate this problem:
If you want to read more details about how this works, keep on reading :).
Certainly, context was a high-value addition to React. As they stated:
Context provides a way to pass data through the component tree without having to pass props down manually at every level, more info about it in this link
This is great! We can make data accessible to many different components regardless its level in the tree, that is to say, we can subscribe components to a "global" data store. Just wrap your app within a context provider and feed that provider with the data you want to make global:
const globalState = {
text: "foo",
};
const globalStateContext = React.createContext(globalState);
const App = () => (
<globalStateContext.Provider value={globalState}>
<...>
</globalStateContext.Provider>
);
Consumption of this data is a piece of cake thanks to the hooks: just retrieve your state by using useContext
hook wherever you need it:
const TextComponent = () => {
const { text } = React.useContext(globalStateContext);
return <p>{text}</p>;
};
In the previous example we just did a basic approach: consuming a static global state. We can't modify text
in any way. However, we could go a bit further and build a more flexible solution as most of your scenarios will require to not only consume global data but also modify it.
For that purpose, let's bring useReducer into the equation. It will provide us with a single and constant dispatch method to trigger global state updates. Then, let's make dispatch
available to our components through a separate context; sort of having 2 "channels", one to consume global data and another one to modify it.
const defaultGlobalState = {
num: 0,
text: "foo",
bool: false
};
const globalStateContext = React.createContext(defaultGlobalState);
const dispatchStateContext = React.createContext(undefined);
const GlobalStateProvider = ({ children }) => {
const [state, dispatch] = React.useReducer(
(state, newValue) => ({ ...state, ...newValue }),
defaultGlobalState
);
return (
<globalStateContext.Provider value={state}>
<dispatchStateContext.Provider value={dispatch}>
{children}
</dispatchStateContext.Provider>
</globalStateContext.Provider>
);
};
const App = () => (
<GlobalStateProvider>
<...>
</GlobalStateProvider>
);
For the sake of usefulness and completeness, we have added a few more properties in our global state: num
, text
and bool
.
In order to consume this global state, we opted for creating a custom hook that exposes both state
and dispatch
in a single call:
const useGlobalState = () => [
React.useContext(GlobalStateContext),
React.useContext(DispatchStateContext)
];
So, accessing global state from a component can be as easy as this:
const Counter = () => {
const [state, dispatch] = useGlobalState();
return (
<button onClick={() => dispatch({ num: state.num + 1 })}>
{state.num}
</button>
);
};
Of course, it is important to note that this is not the only valid implementation to accomplish global state within React and you may have your own.
Even if you just consider the first basic sample with static global state, or our proposal in the previous section, or any other solution of your own based on React context, all of them will suffer from a common problem: useContext
hook will re-render whenever your context is modified. In other words, every component subscribed to our global state will re-render upon a context change.
To better understand the consequences of this issue, now think about the previous Counter
component: it only uses num
property from global state, so it only needs to be informed whenever num
is changed. However, Counter
will re-render upon bool
or text
changes, even if num
remains the same, but Counter
doesn't care about bool
or text
properties!
So, in summary, all our components "connected" to the global state will re-render under any minimal change in that state, even if it does not affect them directly. That's not how Redux worked, right? This may lead to performance issues. Actually, this is the exact reason why Redux dropped their experimental approach of using direct React context.
To better illustrate this issue, we have built a demo for you:
You can check source code here.
I know what you are thinking: it would be desirable to let useContext
subscribe only to a part of the context value (like a selector) to avoid over re-rendering. Before you blame the React team, I will tell you that this topic has an open discussion in the React community. So far, useContext
has been working as designed and the problem is closer to the pattern itself: we are trying to keep unrelated properties under a single context.
Well, Dan Abramov himself got involved in this discussion and offered some tips. If storing everything in a monolith context is problematic, let's split context separating unrelated data.
You can inspect source code here.
This idea of multiple contexts is gaining traction in the community. Ken C Dodds goes a step further and suggests not only splitting context but also keeping state as close to where it's needed as possible. Even though this solution moves away from having a single global context, it is a very interesting approach to manage state natively in React.
Again, inspired by options 2 and 3 by Abramov, we can wrap our components by containers that filter out undesired global state updates. This solution mimicks what was done by Redux containers: listening to global state changes but only passing down the desired part of the state through props. Although containers will re-render on every context update, they are very light and cheap components to minimize performance issues.
Take a look at the code here.
Based on the previous solutions, the correct usage of React context seems to imply breaking it into smaller pieces to avoid performance issues. But, for whatever reason, what if we want to have a complex object in context serving as a global state?
The following approach is built on top of Daishi Kato's react-tracked, a library which is only 1.5kB and has superb performance, aimed to deal with complex objects in context without the hassle of unnecessary re-renders. Now, we could re-write our Counter
example like this:
const Counter = () => {
const [state, dispatch] = useTracked();
return (
<button onClick={() => dispatch({ num: state.num + 1 })}>
{state.num}
</button>
);
};
And Counter
would only re-render whenever state.num
is updated, ignoring the rest of the properties in our global state. Yes I know, this wouldn't be a 100% native solution, but it provides performance and ease of use at a minimum cost, so it's worth checking it out.
Source code is located here.
We have compiled the demos source code from this post in the following Github repo: global-state-react
Application State Management with React: https://kentcdodds.com/blog/application-state-management-with-react
Github facebook/react discussion, Provide more ways to bail out inside Hooks
React tracked library: https://github.com/dai-shi/react-tracked
In this article, we have explored several approaches to manage global state using native React context, dropping complex Redux implementations and embracing the cleanliness of hooks.
We have demonstrated the issue with useContext
, whose subscription-like mechanism triggers a re-render on every state update, regardless of whether that update is significant for our component or not. Finally, we have presented a few implementations to overcome this problem by either splitting context, blocking undesired re-renders with containers or relaying in a third party solution.
We are a team of Front End Developers. If you need coaching or consultancy services, don't hesitate to contact us.
C/ Pintor Martínez Cubells 5 Málaga (Spain)
info@lemoncode.net
+34 693 84 24 54
Copyright 2018 Basefactor. All Rights Reserved.