This is a phrase you can find in many sites and it is considered (?) as valid:
React Context is often used to avoid prop drilling, however it's known that there's a performance issue. When a context value is changed, all components that use
useContext
will re-render.
Moreover:
The React team has proposed a
useSelectedContext
hook to prevent performance issues with Context at scale. There is a community library for that: use-context-selector
However, for me the above does not make any sense. Don't we want to re-render all the components that use useContext
? Absolutely! Once the context value changes, all components using it must re-render. Otherwise, UI won't be in sync with the state. So, what exactly is the performance issue?
We could discuss how not to re-render the other child components of the Context Provider that do not use useContext
and this is achievable (react docs):
<TasksContext.Provider value={tasks}>
<TasksDispatchContext.Provider value={dispatch}>
{children}
</TasksDispatchContext.Provider>
</TasksContext.Provider>
By using the above pattern, we can avoid re-rendering of all child components that do not use useContext
.
To recap: In my opinion, there is no performance issue when using Context the right way. The only components that will re-render are the ones that should re-render. However, almost all references insist on an underlying performance issue and stress that as one of Context's caveats. Am I missing something?
Note: I'll answer your questions and give a final comment on what I could gather on the internet and from personal experience.
TL;DR: The performance issue is a constant reminder that when you use Context API
even if not explicitly writing it you are virtually passing those props to the components that use a Context State and on each state change for every component that state is accessed, those components will be re-rendered.
Answering each question from your post:
Question 1: Don't we want to re-render all the components that use
useContext
?
Yes, that's exactly it, as you stated.
Question 2: So, what exactly is the performance issue?
On prop changes, components re-render. When using Context API
, even if not explicitly passing the Context state as a prop, each state change will trigger a re-render on that component and the child components that depend on or receive that state as a prop. You can read this on this doc such as:
Context provides a way to share values like these between components without having to explicitly pass a prop through every level of the tree
This is not exactly an issue as the docs suggest you use it to store global state that does not change that much such as:
Those are the type of data that:
Question 3: Am I missing something?
Well, as you could already suppose, there are a lot of cases that don't match that type of usage of a "global state" that doesn't change that much. And, for those, there are some other options that could be used to take care of the same set of cases that Context API
solve, but with a lot more overhead at the code. One of them is Redux
, which doesn't have this overhead for the most obvious reason: Redux
creates a parallel store
from your App and doesn't pass the values as props to each and every component. On the other hand, one of the most noticeable overheads is the tooling that the project should have to accommodate that library.
But why people started using Redux
(and other libs) in the first place?
Handling Global State in past versions of React
was a thing. You could solve this with a lot of different ways, opinions, and approaches. Sooner or later, people started to create and use tools and libs that handles that with approaches that were considered "better" for specific or personal reasons.
Later, those tools/libs started to get more complex and with more possible "connectors" or "middlewares" here and there. As an example, one tool that can be added to Redux
to handle requests is the lib called Redux Thunk
, which allows performing requests inside actions (if I'm not wrong) breaking the concept of just writing actions as pure functions from Redux
. Nowadays, with the growth of React Query
/TanStack Query
, the state related to the requests is starting to be handled also as a parallel "global state" even with cache and a lot more features, dropping the usage of Redux Thunk
and Redux
as a consequence to solve the "global state" from requests.
After the release of Context API
to a stable and improved version, people started to use it for a lot of projects and as a Global State manager. And sooner or later everyone started to notice performance issues related to too many re-renders due to several props changing every time, everywhere. Some of them just went back to the Redux
and other libs but for others, turns out that Context API
is very good, practical, involves less overhead, and is embedded with React
if used as it was intended. The requirements for not having to deal with the performance issues are the same as described before:
There are some other options that Context API
works smoothly if you don't nest too many components. As an example: Multi-page Forms if you create the context at the Route
level and not at the App
level. As you also stated:
In my opinion, there is no performance issue when using Context the right way
But you could say that for almost every tool that is used out of its original usage conception.
Edit: After the OP pointed out and after reading about the Context API in the official docs, props are not passed to each and every child, just for those who use the context. And as a consequence: To each and every child component for those components that pass those props to them.
And, answering the question on why Context API
has performance issues, I'm planning to create a repo to reproduce and understand but my bet is: It is probably related to the fact that each Context
is called as "component" and React handles that itself, instead of creating a "parallel structure" such as Redux
/Jotai
, as an example.
To recap: In my opinion, there is no performance issue when using Context the right way. The only components that will re-render are the ones that should re-render.
If your Provider only provides a simple value, your recap is true.
But in reality the Provider often provides a big tree object containing many branches and leaves. However each Consumer may only need a small portion of it, even a single leaf value on that tree.
In this case there is a perf problem, cus the Context API is a whole sale solution. Even if you update a single leaf value, you still need to update the tree root object’s reference in order to signal a change. But that in turn notifies every single Consumer that useContext
.
It’s true that every one of them should be notified of a change, but it’s not true that all of them should re-render. Optimally only those Consumer which depend on the updated leaf value should re-render.
At its current state Context API doesn’t provide any fine grained control over this issue, thus things like use-context-selector
bring back the selector pattern back into our sight.
Fundamentally this is a pub-sub model, and if you don’t have a mechanism to allow subs to decide which channel to tune in, the only thing you can do is to broadcast to all subs about everything. It’s like waking everybody in the neighborhood up just to tell them “Alice got a new mail”, which obviously is not optimal.
The very same problem exists in barebone Redux setup. Which is why selector pattern from react-redux
used to be very popular.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With