- Daishi Kato's Read the Code
- Posts
- The Past and Future of Render Optimization with React Context
The Past and Future of Render Optimization with React Context
From useContextSelector to use(store)
Hi,
First of all, this post is based on uncertainty. Please don’t take any of it too seriously—these are just thoughts and observations based on past work and recent updates from the React team.
The Problem: State and Context in React
Using useState
and useContext
for state management is still common today—and it works fine until you run into Provider hell. This happens when your component tree is wrapped in multiple context providers, and managing them becomes a chore.
One workaround is to collapse multiple providers into a single component using reduceRight
. For example:
🔗 Reference
const CombinedProviders = (props) => {
const providers = [
[Provider1, { value: props.value1 }],
[Provider2, { value: props.value2 }],
[Provider3, { value: props.value3 }],
];
return providers.reduceRight(
(children, [Provider, value]) =>
<Provider {...value}>{children}</Provider>,
props.children
);
};
This helps with composition, but not with dynamic state. If you add a new state and context to the tree, it still rerenders the entire subtree.
The First Solution: useContextSelector
To solve this, RFC #119 proposed useContextSelector
. Before that landed officially, I released a userland version as the use-context-selector
package.
import { createContext, useContextSelector } from 'use-context-selector';
const context = createContext(null);
const Counter1 = () => {
const count1 = useContextSelector(context, (v) => v[0].count1);
const setState = useContextSelector(context, (v) => v[1]);
// ...
};
The main use case here was state. In some cases, that state is held in an external store.
Then Came useSyncExternalStore
React introduced useSyncExternalStore
as an official way to subscribe to external state safely in concurrent rendering:
import { useSyncExternalStore } from 'react';
import { todosStore } from './todoStore.js';
function TodosApp() {
const todos = useSyncExternalStore(
todosStore.subscribe,
todosStore.getSnapshot
);
}
But there’s a caveat: useSyncExternalStore
is great for safe external state syncing, but it doesn’t always play well with concurrent rendering.
🔗 See this issue
What About Selectors?
The React team explored context selector solutions but seemed reluctant to embrace selector-based APIs. (Incidentally, I developed Jotai to avoid selectors while still optimizing rendering.)
Instead, the team seems to prefer composable patterns, such as combining useMemo
with use(context)
. I built an experimental userland version using this idea:
🔗 react18-use
import { createContext, use, useMemo } from 'react18-use';
const MyContext = createContext({ foo: '', count: 0 });
const Component = () => {
const foo = useMemo(() => {
const { foo } = use(MyContext);
return foo;
}, []);
return <p>Foo: {foo} ({Math.random()})</p>;
};
This approach may work better with React Compiler, and it’s certainly an interesting direction.
Looking Ahead: use(store)
In a recent React blog post, the team introduced a mention of a new API: use(store)
. It appears to be a concurrent-compatible variant of useSyncExternalStore
.
const value = use(store);
We don’t know the details yet. But if it works as intended, I expect use(store)
to take priority over useMemo + use(context)
patterns—at least in many cases. The two solve different problems, but if the former is widely adopted, the latter might become a niche technique (except where React Compiler makes it particularly useful).
In Conclusion
We don’t know the future yet—these are just some ideas based on where things seem to be going. But it’s always exciting to explore the evolution of patterns in React.
Happy coding.
Reply