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

or to participate.