Description
After upgrading to react 19, I noticed a performance regression due to unexpected re-rendering in my react app.
I have a setup with a context that gets updated, but some child trees override that context with their own value and do not always update when the outermost context updates.
In react 18, as expected, children consuming the context (either via useContext
or Context.Consumer
) did not re-render when the outermost context value updated, as long as their tree had an additional context provider that kept a stable context value.
In react 19, this behavior is maintained for useContext
, but Context.Consumer
re-renders its children.
React 18 example: https://codesandbox.io/p/sandbox/dqzj8j
React 19 example: https://codesandbox.io/p/sandbox/j9zrsy
The code is the exact same other than the react dep version.
You can see in the React 18 example, both ChildA
and ChildB
never render again as they are wrapped in memo
and the context value does not change in their tree. In the react 19 example though, ChildB
re-renders every time the value in the outermost context updates, even though that context is re-provided for the tree containing ChildB
.
Update 16/06: I've noticed that in react 19, the useContext
example DOES run the component and do most of the work involved, it just doesn't actually commit the render. I observed this by putting a console.log
in the component to see if it's running.
The concern here isn't because we depend on side effects or anything like that, it's that some of our components are unfortunately quite expensive to run, and this context is consumed everywhere!
Until now, we've been running on the assumption that as long as the context value for that subtree remained stable, that components would not re-run (which was true pre react 19)
It took a while for us to understand this as this manifests as work being done to run all the consumers (visible in chrome performance profile), but then the work missing from the flamegraph in react profiler since nothing is actually rendered. (You can see it accounted for in the total render time in the timeline tab of react profiler though)
Here's what we are doing for a workaround right now to avoid the perf penalty. We're creating a second context and using useContext(FooOverride) || use(Foo)
to conditionally call the "main" context.