Skip to content

Conversation

gnoff
Copy link
Contributor

@gnoff gnoff commented Jul 8, 2019

View Rendered Text

This RFC describes a new API, currently envisioned as a new hook, for allowing users to make a selection from a context value instead of the value itself. If a context value changes but the selection does not the host component would not update.

This is a new API and would likely remove the need for observedBits, an as-of-yet unreleased bailout mechanism for existing readContext APIs

For performance and consistency reasons this API would rely on changes to context propagation to make it lazier. See RFC for lazy context propagation

Motivation

In summary, adding this useContextSelector API would solve external state subscription incompatibilities with Concurrent React, eliminate a lot of complexity and code size in userland libraries, make almost any Context-using app the same speed or faster, and provide users with a more ergonomic alternative to the observedBits bailout optimization.

Addendum

Example: https://codesandbox.io/s/react-context-selectors-xzj5v
Implementation: https://github.com/gnoff/react/pull/3/files

@theKashey
Copy link

User space implementation - https://github.com/dai-shi/use-context-selector

@dantman
Copy link

dantman commented Jul 17, 2019

Hello @gnoff, I posted an alternative API idea awhile ago but never got around to writing an RFC for it and I'd like to get your opinion on it.

The idea was instead of adding a hooks specific selector was to allow creating "slices" of the contexts, which can then be used with any context api. The basic api was MyContext.slice(selector), i.e. contexts would have a new .slice method which would return a new context object. The original idea was in this comment.
Some examples:

// Static usage (possibly allows for extra internal optimizations?)
const EmailContext = UserContext.slice(user => user.email);
let MyComponent() {
  const email = useContext(EmailContext);
  // ...
}

// Dynamic usage with hooks
const KeyContext = useMemo(() => MyContext.slice(value => value[key]), [key])
const keyValue = useContext(KeyContext);

// Static usage with contextType
const EmailContext = UserContext.slice(user => user.email);
class MyComponent extends Component {
  static contextType = EmailContext;
  // ...
}

// Deep slices (imagine this is split up between different layers of an app)
const UserContext = AppState.slice(appState => appState.currentUser);
const EmailContext = UserContext.slice(user => user.email);
let MyComponent() {
  const email = useContext(EmailContext);
  // ...
}

I will admit there is one small flaw in my idea I didn't realize before. I thought this could be used universally, i.e. MyContext.slice could provide a selectable version of context that could be used across useContext, contextType, and <Consumer> and only contextType would have the limitation of being static-only. So I originally had some examples of using this with the consumer API. However I just realized that the slice api cannot be used dynamically with the consumer API. Because when you change the selector and get a new context back <SlicedContext.Consumer> will be a different component and as a result you'll get a full remount of everything inside it.

@j-f1
Copy link

j-f1 commented Jul 17, 2019

You could work around the issue with <Consumer /> by making a custom component that uses useContext to create a HOC matching Context.Consumer’s API.

@dantman
Copy link

dantman commented Jul 17, 2019

@j-f1 Interesting idea, though rather than HOC I presume you mean render function.

let Consumer = ({context, children}) => {
  const data = useContext(context);
  return children(data);
};

const KeyContext = SomeContext.slice(value => value[key]);
render(<Consumer context={KeyContext}>{keyValue => <span>{keyValue}</span>}</Consumer>);

You know, I'd almost think of recommending using something like that everywhere even in non-sliced contexts. The context API is really strange now that you think about it. When context was released <Context.Consumer> was the only way to consume a context and the best practice was to export just the consumer so you could control the provider. But now useContext and contextType instead take the context object directly. Which makes using a consumer off the context, it would make much more sense if you passed the context to the consumer instead of using a consumer from a context.

@gnoff
Copy link
Contributor Author

gnoff commented Jul 19, 2019

@dantman Interesting idea for sure. I think the way context is currently implemented internally would make some of the really dynamic stuff hard but the static stuff could probably be implemented in userland with some clever user of providers and consumers

One of the things that I don't think can be done in userland without changing the context propagation to run a bit lazier is to safely use props inside selectors for context. The issue is that during an update work may be done to change the prop so if a selector runs too early it will do so with the current prop and not the next one. this can lead to extra work being done but also errors in complicated cases where a context update is the thing itself that would change the prop.

The focus of my rfc and implementation PR is on hooks because it is the most dynamic use case. It would be relatively straight forward to add selector support for Consumer and contextType as well

As for your slice API I think you can also more or less create that on top of this without some of the challenges of creating dynamic contexts just by layering selectors and passing the result into new contexts as values

One more thing to point out, in one of my Alternatives I mention something that I think is genuinely a bit novel

// take in multiple context values, only re-render when the selected value changes
// in this case only when one of the three contexts is falsey vs when they are all truthy)
useContexts([MyContext1, MyContext2, MyContext3], (v1, v2, v3) => Boolean(v1 && v2 && v3))

Every other context optimization I've seen can only work on single context evaluations, and while I've not implemented the above api it is readily within grasp given how useContextSelector is implemented

It's almost more like a useComputedValue but by virtue of how react manages updates it can only reasonably react to changes in context values and not values in general

@PedroBern
Copy link

PedroBern commented Oct 29, 2019

Hello there, Im pasting an answer I gave originally in stackoverflow about context rerendering, what you think?

It is one way to use selectors with the context. Maybe it helps to build this api.

original link

There are some ways to avoid re-renders, also make your state management "redux-like". I will show you how I've been doing, it far from being a redux, because redux offer so many functionalities that aren't so trivial to implement, like the ability to dispatch actions to any reducer from any actions or the combineReducers and so many others.

Create your reducer

export const reducer = (state, action) => {
  ...
};

Create your ContextProvider component

export const AppContext = React.createContext({someDefaultValue})

export function ContextProvider(props) {

  const [state, dispatch] = useReducer(reducer, {
    someValue: someValue,
    someOtherValue: someOtherValue,
    setSomeValue: input => dispatch('something'),
  })

  return (
    <AppContext.Provider value={context}>
      {props.children}
    </AppContext.Provider>
  );
}

Use your ContextProvider at top level of your App, or where you want it

function App(props) {
  ...
  return(
    <AppContext>
      ...
    </AppContext>
  )
}

Write components as pure functional component

This way they will only re-render when those specific dependencies update with new values

const MyComponent = React.memo(({
    somePropFromContext,
    setSomePropFromContext,
    otherPropFromContext, 
    someRegularPropNotFromContext,  
}) => {
    ... // regular component logic
    return(
        ... // regular component return
    )
});

Have a function to select props from context (like redux map...)

function select(){
  const { someValue, otherValue, setSomeValue } = useContext(AppContext);
  return {
    somePropFromContext: someValue,
    setSomePropFromContext: setSomeValue,
    otherPropFromContext: otherValue,
  }
}

Write a connectToContext HOC

function connectToContext(WrappedComponent, select){
  return function(props){
    const selectors = select();
    return <WrappedComponent {...selectors} {...props}/>
  }
}

Put it all together

import connectToContext from ...
import AppContext from ...

const MyComponent = React.memo(...
  ...
)

function select(){
  ...
}

export default connectToContext(MyComponent, select)

Usage

<MyComponent someRegularPropNotFromContext={something} />

//inside MyComponent:
...
  <button onClick={input => setSomeValueFromContext(input)}>...
...

Demo that I did on other StackOverflow question

Demo on codesandbox

The re-render avoided

MyComponent will re-render only if the specifics props from context updates with a new value, else it will stay there.
The code inside select will run every time any value from context updates, but it does nothing and is cheap.

Other solutions

I suggest check this out Preventing rerenders with React.memo and useContext hook.

@gnoff
Copy link
Contributor Author

gnoff commented Oct 29, 2019

@PedroBern thanks for your input

I think your advice here is useful for a certain kind of optimization, even a very common one, however it does not address the main performance issue that useContextSelector is trying to eliminate.

The issue is that in your example there is a component which re-renders, the HOC that wraps MyComponent. It even tries to render MyComponent but because you have React.memo wrapped around it you bail out of rendering. The problem is with certain kinds of context updates where there may be thousands of HOCs for a single update, even this limited render is relatively expensive.

Using an implementation of react-redux that relies on context to propagate state changes you can see this by comparing a version using useContext and a version using useContextSelector. In my personal testing I was seeing update times of 40ms for useContext and 4ms for useContextSelector. This tenfold increase means the difference between jank and smooth animations.

In addition to improving performance across the board over useContext it also requires much less code to write what you want to write. For instance React.memo is required in your given solution but does not matter with useContextSelector. Also React.memo may not actually be tenable if you want to use Maps, Sets, and other mutative objects without opting into expensive copying.

Lastly I'll say that HOCs are really nice but don't compose nearly as well as hooks do so when you want to consume data from multiple contexts hook composition can be much more ergonomic.

I hope that clarifies why this RFC provides values not currently possible given existing techniques

Thanks,
Josh

@GasimGasimzada
Copy link

I like this API a lot because it focuses more on selecting values instead of bailing out of updates. I would suggest that, instead of using a separate hook, it would be more beneficial if existing useContext hook accepts function as a second argument (or is there a specific issue where this is not a good idea?) Additionally, this can work with all React concepts that support context:

class MyComponent extends React.Component {
  static contextType = MyContext;
  static contextSelector = value => value.name
}

<MyContext.Consumer selector={value => value.name}>
  ...
</MyContext.Consumer>

@sebmarkbage
Copy link
Collaborator

sebmarkbage commented Jan 6, 2020

One general optimization we could do is to eagerly call the render function during propagation and see if the render yields a different result and only if it does do we clone the path to it.

Another way to look at this selector is that it's just a way to scope a part of that eager render. Another way could be to have a hook that isolates only part of the rerender.

let x = expensiveFn(props);
let y = useIsolation(() => {
   let v = useContext(MyContext);
   return selector(v);
});
return <div>{x + y}</div>;

The interesting thing about this is that it could also be used together with state. Only if this context has changed or if this state has changed and that results in a new value, do we rerender.

let x = expensiveFn(props);
let y = useIsolation(() => {
   let [s, ...] = useState(...);
   let v = useContext(MyContext);
   return selector(v, s);
});
return <div>{x + y}</div>;

Another way to implement the same effect is to just memoize everything else instead.

let x = useMemo(() => expensiveFn(props), [props]);
let [s, ...] = useState(...);
let v = useContext(MyContext);
let y = useMemo(() => selector(v, s), [v, s]);
return useMemo(() => <div>{x + y}</div>, [x, y]);

It's hard to rely on everything else being memoized today. However, ultimately I think that this is where we're going. Albeit perhaps automated (e.g. compiler driven).

If that is the case, I wonder if this API will in fact be necessary or if it's just something that we get naturally for free by memoizing more or the other parts of a render function.

@dai-shi
Copy link

dai-shi commented Jan 6, 2020

useIsolation would be wonderful. The memoization technique may result the same effect, but it would be difficult to create a custom hook, I suppose.

@theKashey
Copy link

Hooks has a little design flaw - while they are "small" and "self-contained", their combination is not, and might update semi-randomly with updates originated from different parts.
useIsolation might solve this problem.

@GasimGasimzada
Copy link

Doesn’t this break the Rules of Hooks? If this is implemented, isn’t it going to confuse developers, especially newcomers: “do not call Hooks inside nested functions but you can call it inside useIsolation.”

@gnoff
Copy link
Contributor Author

gnoff commented Jan 7, 2020

@sebmarkbage

One general optimization we could do is to eagerly call the render function during propagation and see if the render yields a different result and only if it does do we clone the path to it.

Unless i misunderstand this would still rely on the function memoizing the rendered result otherwise equivalent renders would still have different results. Then we'll get into territory where everyone is always memoizing rendered results as a lazy opt-in to maybe sometimes more efficient context propagation. Also unless you pair this with lazy context propagation your render will be using previous props so you may end up calling it multiple time in a single work loop with different props and recompute memo'd values / expensive functions.

The thing that makes useContextSelector fast is that it guarantees single execution because it defers to work that would have been done anyway and ONLY does context selector checking if all regular work has been exhausted

Another way could be to have a hook that isolates only part of the rerender.

This is super cool. it's like useContexts(Context1, Context2, ...) but it can also use state and is more ergonomic. In theory you could actually nest them so there are isolations within your isolation so they can still compose nicely with custom hooks.

Again though, unless you have lazy context propagation things like useReducer are going to deop a lot b/c the props you see during the propagation call are not necessarily the ones you will get once you do the render

I understand the concerns around rules of hooks etc... and it is definitely a little confusing to teach the 'exception' in a way but payoff may be worth it.

The biggest downside I see is cognitive load. useContextSelector is pretty WYSIWYG. useIsolation is named after what it does in a way but only if you understand what React is doing. may be better to do useValue or something

interestingly there may be a way to combine this with useMemo so we don't need a new hook. if you start using state and contexts inside memos they can reset the cache and schedule work allowing them to trigger updates but the dependency array semantics can work for all other normal cases where internal state / context values haven't changed.

@mgenev
Copy link

mgenev commented Jan 15, 2020

Doesn’t this break the Rules of Hooks? If this is implemented, isn’t it going to confuse developers, especially newcomers: “do not call Hooks inside nested functions but you can call it inside useIsolation.”

Personally, it does not confuse me. It makes everything much better and I'm adopting the userland module for it already.

samholmes added a commit to EdgeApp/edge-react-gui that referenced this pull request Jan 30, 2024
This user-land library improves performance by removing unnecessary
re-renders to components which use global contexts. By selecting the
state that the components use from the contexts, they'll limit their
re-rendering to only when that state changes.

See reactjs/rfcs#119 for more relevant
information about the performance issues with the context API.
@rostero1
Copy link

Is this going to be the solution for React 19?

const useFoo = () => {
  return useMemo(() => {
    const { foo } = use(MyContext)
    return foo
  }, [])
}

Reference

@stephan-noel
Copy link

This won't be supported in the initial release of React 19 it seems:

image

Source

Also I think they're looking to optimize out useMemo:

image

Source

I realize this probably isn't a priority right now, but if there is space for helping out, I'd be glad to do so in whatever form.

I'm also interested in if it's being blocked by Forget and if there will be a way to still use useMemo in the cases where Forget may not be able to compile properly due to breaking some of the compiler-specific rules (ie, reactjs/react.dev#6680 (comment)).

@eduardocque
Copy link

hi @gnoff any news about this RFC, i believe this will be a game changes for react and context/provider API, for complex aplications this will be nice for performance and re-rendering perspective

@wisefool769
Copy link

hi @gnoff please don't release this, it will put an end to an entire cottage industry of state management libraries

@mindplay-dk
Copy link

hi @gnoff please don't release this, it will put an end to an entire cottage industry of state management libraries

Jokes aside, it really would - and this would be so much better, because it would work "out of the box" with every context/provider in every existing project and third-party library.

It's a much more natural fit for the ecosystem than any state management library, all of which replace, compete, or overlap with concerns already handled by native React features and concepts - instead, this leverages what's already there.

In fact, I might go as far as to suggest to just add this as an optional, second argument to useContext - that would be a non-breaking change, and near-zero learning curve, and you could immediately start optimizing by just adding selectors.

Not only does this serve as an optimization, but I think it also leads to more readable and declarative code - now you can actually see what a component depends on, without having to read through the entire body to see which context props it's accessing.

This is the "missing link", if you ask me.

I really hope this doesn't need to sit on a shelf for another 5 years before the team moves on this. 😅

@michaelfaith
Copy link

+1 This would be huge.

@markerikson
Copy link

@mindplay-dk : fwiw the React team has said that they never found a viable API design for the original context selectors concept, and that the briefly-conceived "use in useMemo" idea is being dropped, because the React Compiler provides essentially the same benefits. Components consuming a context will still re-render, but the Compiler will memoize all of their child output. So, if a component only relies on a piece of a context that didn't change, none of its children will re-render.

@josephsavona
Copy link
Member

josephsavona commented Aug 22, 2025

A few key realizations that we've had since revisiting this post:

First, as @sebmarkbage noted above, memoizing everything is a pretty compelling answer. React Compiler is close to stable and now makes the approach of relying on memoization viable, see previous comment for an example.

Second, we've also done a ton of benchmarking and exploration on top of the compiler. A key finding is that any reactive system is going to have a hard time with a single immutable blob of state that you "select" pieces out of. By "blob" I mean something like a large object that contains all the data for your app, or a substantial portion of the data for your app. Imagine having a Store object that has all the entities in it for the app, each component pulls from them, and you do copy-on-write style immutable updates whenever anything changes. Any reactive UI framework will end up with a computation graph where every little change to such a blob-state invalidates all the downstream computation, and triggers at least a recheck. This is why really fast reactive systems don't use a single large blob of state!

The key to making systems fast is better data modeling. Signals are one way to do that, but they push you into explicit meta-programming. There are other ways to do better data modeling. Relay, for example, uses a normalized store over which we run selectors (fragments are selectors). The store isn't one giant immutable blob — it's normalized — and when parts of the store change we can eagerly evaluate which subset of selectors may have changed. This gives us more control than signals since we tune the granularity of subscriptions (per-field? per-record? per-type?) to balance update cost vs tracking overhead (we actually "per record but with tricks").

This is why one of our major areas of focus is on supporting concurrent stores with selectors: to make it easier for developers to use better data models and not rely on context. You might use context to pass down which store to read from, but not to access the store itself. This is all a bit in flux and details may change, but we're excited that we have a much better understanding of the problem space. Context selector and signals have all the mindshare, but focusing on the fundamentals has lead us to a solution we're really excited about. More to come.

@mindplay-dk
Copy link

Components consuming a context will still re-render, but the Compiler will memoize all of their child output. So, if a component only relies on a piece of a context that didn't change, none of its children will re-render.

As far as I can tell (from the example posted by @josephsavona) this memoizes computations after the context has already caused the component to run?

To my understanding, the proposed useContextSelector (as implemented by @dai-shi) instead retains the selector function, and manages the context subscription, in such as way that only the context selector function itself will be re-run when the context is updated? If I understand correctly, that means a subscribing component doesn't run at all unless the result from the context selector changes?

React compiler memoizing things will no doubt help performance overall, but doesn't seem to address the same issue?

A key finding is that any reactive system is going to have a hard time with a single immutable blob of state that you "select" pieces out of. By "blob" I mean something like a large object that contains all the data for your app, or a substantial portion of the data for your app.

Putting your entire app state in one container is obviously just bad architecture.

But in the case where I put all of the state for a screen with 20 different components inside a single context, and each of them subscribe to just a few of 50 different properties in that context, based on that compiler output, all of those components would still be triggered by the useContext call - even if they're internally optimized in terms of the computations they perform, any time you enter a keystroke into a search input or something, that's still 20 components running, producing VNodes and getting diffed, is it not? Even if all of those components are now "fast", having them run in the first place isn't ideal.

I want the freedom to architect and layer components in a way that makes practical sense, without micro performance concerns influencing those decisions, taking up time, or driving complexity.

I get that with Zustand right now, and a complex screen like the one I described here creates no problems - but the problem on a screen like this one is not the performance of the individual components; most of those aren't doing much other than selecting from the store, doing minimal view-related logic, and returning VNodes in the first place. The overhead in this case is mostly excessive unnecessary diffing, and selectors fix that.

I haven't actually tested use-context-selector, so I don't know how it compares to a store-based approach.

This is why one of our major areas of focus is on supporting concurrent stores with selectors: to make it easier for developers to use better data models and not rely on context

@josephsavona does React compiler use some type of stores as well? are you referring to an internal feature of the compiler, or are there plans to introduce a native store of some sort in React as well? I haven't heard anything about this.

@markerikson
Copy link

@mindplay-dk : that's what I was saying.

Yes, if we start by assuming that you've properly memoized the direct child of the context provider so that it doesn't cascade through the whole tree by default, at that point what will happen is:

  1. React scans the tree for all consumers of the context
  2. Each context-consuming component will re-render
  3. React resumes its usual recursion from there, and renders every component nested inside those consumers

With the React Compiler, you still have steps 1 and 2 happening. Every component that consumes the updated context will still re-render.

What changes is 3). By memoizing the render output of those components, only the consumers that actually used updated pieces of the context will render updated children. This eliminates the bulk of the extra re-rendering that would have happened by default.

To give a fictional example, say we have 5 context-consuming components (ABCDE), each accessing one field in an {a, b, c, d, e} object, and each with 10 children inside. The context provider does a setState({...state, b: newValue}).

With React's default behavior:

  • All 5 context-consuming components render
  • All 50 nested children render

so 55 components rendered.

With the Compiler:

  • All 5 context-consuming components render
  • The 10 children nested under B render
  • but the 40 children under ACDE do not

so only 15 components rendered

So, overall the Compiler should significantly cut down on how many nested components re-render when a context updates.

You're right that this is not as optimized as a hypothetical context selectors API. But:

  • A context selectors API would still require at least running those selectors. Most of them would probably still be straightforward getters (value => value.b), but it's still function calls
  • It would require additional API design work to nail down the behavior. Doable, but not free
  • The React compiler ought to make the cost of running a component function cheaper, because the memoization checks should eliminate creating elements that didn't change. It also then eliminates the cost of rendering those unchanged nested children.

So, the Compiler does provide most of the benefit of what a context selectors API would have given you perf-wise, but with no API changes needed.

Overall, perf needs will vary, but my sense is that the real problem with Context perf has been the recursive re-rendering, and the Compiler addresses that part.

does React compiler use some type of stores as well? are you referring to an internal feature of the compiler

No. This is a reference to an actual new API that's being planned:

@josephsavona
Copy link
Member

Does React compiler use some type of stores as well? are you referring to an internal feature of the compiler, or are there plans to introduce a native store of some sort in React as well? I haven't heard anything about this.

Clarifying @markerikson's answer on this — this PR linked to is one direction we are exploring within this space but highly likely to change.

Putting your entire app state in one container is obviously just bad architecture.
But in the case where I put all of the state for a screen with 20 different components inside a single context, and each of them subscribe to just a few of 50 different properties in that context

The key thing to recognize here is that the problem is fractal: large blobs of state are hard for reactive systems to optimize. The more targeted the change is, the less work that has to happen downstream. This is true for any reactive system.

The context and selector API is fundamentally based on this pattern:

  • Update giant blob of context (whole app, whole screen, whole subsytem, whatever)
  • Re-run all selectors

That's already suboptimal. The API I think we need to get to is that the Store can do its own internal tracking and tell React which components updated. This is a key part of why Relay helps us with performance — because Relay isn't running all selectors on every store change: some part of the store changes, Relay filters down to a subset of possibly affected selectors, runs those, and then only informs React about the selectors whose output changed. We want to provide a generic mechanism for stores like that to integrate with React in a concurrent compatible way.

@mindplay-dk
Copy link

With the Compiler:

  • All 5 context-consuming components render
  • The 10 children nested under B render
  • but the 40 children under ACDE do not

so only 15 components rendered

but this is assuming the deeper children aren't also subscribed to that context, right?

quoting myself:

a screen with 20 different components inside a single context, and each of them subscribe to just a few of 50 different properties in that context, based on that compiler output, all of those components would still be triggered by the useContext call

it doesn't help in this case, does it?

if I understand correctly, it would optimize "leaf nodes", e.g. inputs and date-pickers rendered by the actual components of the screen itself - but it doesn't help for the components that are actually subscribed to the context, does it?

also, memoizing the VNodes in every single component instance is going to require a lot more memory, isn't it? I mean, as compared with useContextSelector, which would only memoize the selected value, as opposed to all the nodes rendered by it?

the introduction of stores sounds a lot more efficient in that case - because it would memoize at the level of the store, rather than at the level of the individual components using the store, right? this is part of what we get with e.g. Zustand as well.

I guess, if React shipped another performance-tuning feature that doesn't compete with existing third-party libs, it's unlikely this would motivate me to replace my current state management lib.

whereas, if you were to ship something like context selectors that removes the need for performance-tuning and improves it naturally, removing the need for third-party stores, even improving the readability of the code (by clarifying the precision of context dependencies directly where those dependencies are introduced) this would definitely be motivation to switch - the clear benefit of context selectors, in my opinion, is you could argue for the use of them solely based on things like code readability without even considering performance details, which would just be a positive side effect.

@klis87
Copy link

klis87 commented Aug 26, 2025

if I understand correctly, it would optimize "leaf nodes", e.g. inputs and date-pickers rendered by the actual components of the screen itself - but it doesn't help for the components that are actually subscribed to the context, does it?

If this is true, then it is very worrying. It reminds me concepts of smart and dumb components. But I thought the community mostly abandonned this concept in favour of subscribing to data in more nodes, so that you do not end up with those bloated smart components which pulls data just to pass it down to its multiple children. If a child node only needs a data, why to get it from the parent? Collocation is more fancy this days, personally I waited so much for this RFC feature


Another thing, I also thought that memoizing is a cost, and you should not memoize components, which are small, as this could actually cause performance to downgrade. Why to memoize a component which will usually rerender anyway?

@josephsavona
Copy link
Member

josephsavona commented Aug 26, 2025

it doesn't help in this case, does it?

It does. I'd encourage you to watch the compiler talks we gave at React Conference 2024, really read @markerikson's explanation above, or take a closer look at the example I linked. If you look closely at the output code, you can see that when the StoreContext changes, the component won't do anything meaningful unless store[props.id].name changes. If that slice of the context doesn't change, all the slow work is skipped:

import { c as _c } from "react/compiler-runtime";
function Component(props) {
  const $ = _c(5);
  const store = useContext(StoreContext);
  const item = store[props.id]; // cheap
  let t0;
  if ($[0] !== item.name) { // skipped!
    t0 = greet(item.name);
    $[0] = item.name;
    $[1] = t0;
  } else {
    t0 = $[1];
  }
  const greeting = t0;
  let t1;
  if ($[2] === Symbol.for("react.memo_cache_sentinel")) { // cached once and skipped thereafter
    t1 = <Header />;
    $[2] = t1;
  } else {
    t1 = $[2];
  }
  let t2;
  if ($[3] !== greeting) { // skipped
    t2 = (
      <>
        {t1}
        <Greeting value={greeting} />; // entire subtree skipped unless it has state/context changes
      </>
    );
    $[3] = greeting;
    $[4] = t2;
  } else {
    t2 = $[4];
  }
  return t2;
}

Now you might say, "but you still have to run store[props.id] every time the context changes". Yes, true. That's also very fast compared to calling greet(item.name) and doing all the downstream work unnecessarily. There is some overhead in any form of incremental computation. What the compiler is doing is allowing your code to, in general, zip along the path from the root to where there are actual changes, even within component and hook bodies.

@theKashey
Copy link

Just to double check - while compiler will optimise rendering caused by Context update, the subscription-less nature of it will still be causing whole sub tree traverse in search of consumers.
Ie facebook/react#13739

@gaearon
Copy link
Member

gaearon commented Aug 29, 2025

Note that traversal has been optimized (facebook/react#20890 shipped in 19).

@mindplay-dk
Copy link

@josephsavona "all the slow work is skipped" - you don't know that, do you? There could be calls to a dozen hooks or other heavy computations before we get to useContext.

What that sounds like to me, is I will still need to think carefully about performance implications of trivial things, like the order in which I'm making calls or computations, in every component? Are you sure you're trying to rationalize the problem away?

@phryneas
Copy link

phryneas commented Aug 31, 2025

@josephsavona "all the slow work is skipped" - you don't know that, do you? There could be calls to a dozen hooks or other heavy computations before we get to useContext.

If you use the compiler, those won't execute unless inputs changed, which (for an unrelated context update) would mean you have side effects that violate the rules of React.

@josephsavona
Copy link
Member

@mindplay-dk You keep on making the same assertion that things will work in a certain way with the compiler without showing any concrete examples to back up your claim. In contrast, we’ve shared code examples that disprove your hypothesis: see again the code snippet I sent. Try playing with it!

Our argument is the compiler already achieves a lot of what people want from context selectors. Further, our research shows that context selectors aren’t a great solution to the problem people want them to solve: they can still do too much work.

We’re open to new information, but the claims here run counter to what we have observed in production and local experiments. Anyone who wants to continue the discussion here, I’d ask that you please create a benchmark. Show up w actual working code where the compiler isn’t enough. I’m sure there are cases that we don’t optimize fully, and we’d love to see more real world examples to help us optimize more. We’re open to new data but need actual data, not opinions.

@eduardocque
Copy link

@josephsavona what about if we dont want to use react compiler and we want to manage it ourself ? are there proposals for those case that dont want to use it ?

@josephsavona
Copy link
Member

@eduardocque If there is a technical blocker that prevents you from adopting the compiler, please file an issue and we’ll look into it.

@eduardocque
Copy link

eduardocque commented Aug 31, 2025

@josephsavona to be honest my project its really complicated to submit an issue, and thats why im not even asking to fix the compiler to support my complex scenarios, instead of that i want to be able to use this proposal from my side

tbh im scare to use this kind of tools/plugins for projects with more than 100k lines of code

Note: lets keep on mind that react-compiler is optional, nobody should be forced to use it (if that happens, means that something wrong its there)

Regards
Carlos

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.