Skip to content

[Experiment] Context Selectors#3

Open
everettbu wants to merge 5 commits intomainfrom
context-selectors
Open

[Experiment] Context Selectors#3
everettbu wants to merge 5 commits intomainfrom
context-selectors

Conversation

@everettbu
Copy link

@everettbu everettbu commented Dec 12, 2025

Mirror of facebook/react#20646
Original author: acdlite


Based on #20890

This is not a final API. It's meant for internal experimentation only. If we land this feature in our stable release channel, it will likely differ from the version presented here.

This implements unstable_useContextSelector behind a feature flag. It's based on RFC 119 and RFC 118 by @gnoff.

Usage:

const context = useContextSelector(Context, c => c.selectedField);

The key feature is that if the selected value does not change between renders, the component will bail out of rendering its children, a la memo, PureComponent, or the useState bailout mechanism. (Unless some other state, props, or context was updated in the same render.)

One difference from the RFC is that it does not return the selected value. It returns the full context object. This serves a few purposes: it discourages you from creating any new objects or derived values inside the selector, because it'll get thrown out regardless. Instead, all the selector will do is return a subfield. Then you can compute the derived value inside the component, and if needed, you memoize that derived value with useMemo.

If all the selectors do is access a subfield, they're (theoretically) fast enough that we can call them during the propagation scan and bail out really early, without having to visit the component during the render phase.

Another benefit is that it's API compatible with useContext. So we can put it behind a flag that falls back to regular useContext.

The longer term vision is that these optimizations (in addition to other memoization checks, like useMemo and useCallback) are inserted automatically by a compiler. So you would write code like this:

const {a, b} = useContext(Context);
const derived = computeDerived(a, b);

and it would get converted to something like this:

const {a} = useContextSelector(Context, context => context.a);
const {b} = useContextSelector(Context, context => context.b);
const derived = useMemo(() => computeDerived(a, b), [a, b]);

(Though not this exactly. Some lower level compiler output target.)

acdlite added 5 commits July 10, 2021 12:59
This block is getting hard to read so I moved it to a separate function.
I'm about to refactor the logic that wraps around this path.

Ideally this early bailout path would happen before the begin phase
phase. Perhaps during reconcilation of the parent fiber's children.
The only reason we pass `updateLanes` to some begin functions is to
check if we can perform an early bail out. But this is also available
as `current.lanes`, so we can read it from there instead.

I think the only reason we didn't do it this way originally is because
components that have two phases — error and Suspense boundaries —
use `workInProgress.lanes` to prevent a bail out, since during the
initial render there is no `current`. But we can check the `DidCapture`
flag instead, which we use elsewhere to detect the second phase.
For internal experimentation only.

This implements `unstable_useContextSelector` behind a feature flag.
It's based on [RFC 119](reactjs/rfcs#119) and
[RFC 118](reactjs/rfcs#118) by @gnoff.

Usage:

```js
const context = useContextSelector(Context, c => c.selectedField);
```

The key feature is that if the selected value does not change between
renders, the component will bail out of rendering its children, a la
`memo`, `PureComponent`, or the `useState` bailout mechanism. (Unless
some other state, props, or context was updated in the same render.)

One difference from the RFC is that it does not return the selected
value. It returns the full context object. This serves a few purposes:
it discourages you from creating any new objects or derived values
inside the selector, because it'll get thrown out regardless. Instead,
all the selector will do is return a subfield. Then you can compute
the derived value inside the component, and if needed, you memoize that
derived value with `useMemo`.

If all the selectors do is access a subfield, they're (theoretically)
fast enough that we can call them during the propagation scan and bail
out really early, without having to visit the component during the
render phase.

Another benefit is that it's API compatible with `useContext`. So we can
put it behind a flag that falls back to regular `useContext`.

The longer term vision is that these optimizations (in addition to other
memoization checks, like `useMemo` and `useCallback`) are inserted
automatically by a compiler. So you would write code like this:

```js
const {a, b} = useContext(Context);
const derived = computeDerived(a, b);
```

and it would get converted to something like this:

```js
const {a} = useContextSelector(Context, context => context.a);
const {b} = useContextSelector(Context, context => context.b);
const derived = useMemo(() => computeDerived(a, b), [a, b]);
```

(Though not this exactly. Some lower level compiler output target.)
This will to make it easier to A/B test, or to revert if we abandon the
experiment. Using a selector will not change the return type of
`useContext`. Use a userspace hook to get the selected value:

```js
function useContextSelector<C, S>(Context: C, selector: C => S): S {
  const context = useContext(Context, {unstable_selector: selector});
  const selectedContext = selector(context);
  return selectedContext;
}
```
@everettbu everettbu added CLA Signed React Core Team Opened by a member of the React Core Team labels Dec 12, 2025
Copy link

@greptile-apps-staging greptile-apps-staging bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.

@greptile-apps
Copy link

greptile-apps bot commented Dec 12, 2025

Greptile Overview

Greptile Summary

This PR implements an experimental context selectors feature that allows components to subscribe to specific fields of a context value and bail out of re-renders when only unselected fields change.

Critical Issue Found:

  • Both ReactFiberNewContext.new.js and ReactFiberNewContext.old.js contain a critical bug at line 358 where the selector is double-applied during bailout comparison: is(oldSelectedValue, selector(newSelectedValue)) should be is(oldSelectedValue, newSelectedValue). This breaks the entire bailout optimization.

Architecture:

  • Adds optional unstable_selector parameter to useContext that accepts a selector function
  • Stores both the full context value and selected value in ContextDependency
  • During context propagation, runs selectors and compares selected values to enable early bailout
  • Integrates with lazy context propagation system via checkIfContextChanged in BeginWork

Implementation Quality:

  • Hook implementation in ReactFiberHooks is clean and properly delegates between readContext and readContextWithSelector
  • The checkScheduledUpdateOrContext helper properly integrates selector checks into the bailout logic
  • Test coverage is comprehensive with good edge case testing (mixed context types, same component multiple contexts)
  • Type definitions properly extend ContextDependency with selector and selectedValue fields

Confidence Score: 0/5

  • This PR contains a critical bug that completely breaks the core optimization feature
  • The double-applied selector bug at line 358 in both ReactFiberNewContext files will cause the bailout optimization to fail. When comparing is(oldSelectedValue, selector(newSelectedValue)), it's applying the selector to an already-selected value, which will produce incorrect results (e.g., calling context => context.a on {a: 0} produces 0, then calling it again on 0 produces undefined). This makes the entire feature non-functional.
  • packages/react-reconciler/src/ReactFiberNewContext.new.js and packages/react-reconciler/src/ReactFiberNewContext.old.js both require immediate fix at line 358

Important Files Changed

File Analysis

Filename Score Overview
packages/react-reconciler/src/ReactFiberNewContext.new.js 0/5 Critical bug in selector comparison logic at line 358 - calls selector(newSelectedValue) instead of comparing with newSelectedValue, breaking bailout optimization
packages/react-reconciler/src/ReactFiberNewContext.old.js 0/5 Same critical bug as new.js at line 358 - incorrect selector comparison breaks bailout optimization
packages/react-reconciler/src/ReactFiberHooks.new.js 5/5 Clean implementation of useContext with optional selector parameter, correctly delegates to readContextWithSelector
packages/react-reconciler/src/ReactFiberBeginWork.new.js 5/5 Adds checkScheduledUpdateOrContext helper to check for context changes during bailout, properly integrates with lazy context propagation
packages/react-reconciler/src/tests/ReactContextSelectors-test.js 5/5 Comprehensive test coverage for context selectors including basic usage, mixed context types, and bailout behavior

Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Additional Comments (2)

  1. packages/react-reconciler/src/ReactFiberNewContext.new.js, line 358 (link)

    logic: calling selector(newSelectedValue) double-applies the selector. newSelectedValue is already selector(newValue) from line 356. this should compare newSelectedValue with oldSelectedValue directly.

  2. packages/react-reconciler/src/ReactFiberNewContext.old.js, line 358 (link)

    logic: calling selector(newSelectedValue) double-applies the selector. newSelectedValue is already selector(newValue) from line 356. this should compare newSelectedValue with oldSelectedValue directly.

13 files reviewed, 2 comments

Edit Code Review Agent Settings | Greptile

everettbu pushed a commit that referenced this pull request Feb 17, 2026
* Initial plan

* Add security limits for DoS and RCE prevention in React Server Components

Co-authored-by: dill-lk <241706614+dill-lk@users.noreply.github.com>

* Adjust security limits to be more reasonable and add comprehensive tests

Co-authored-by: dill-lk <241706614+dill-lk@users.noreply.github.com>

* Fix security tests to work within test environment constraints

Co-authored-by: dill-lk <241706614+dill-lk@users.noreply.github.com>

* Fix trailing whitespace issues

Co-authored-by: dill-lk <241706614+dill-lk@users.noreply.github.com>

* Add comprehensive maintainer documentation for security fixes

Co-authored-by: dill-lk <241706614+dill-lk@users.noreply.github.com>

* Add master documentation index for maintainers

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: dill-lk <241706614+dill-lk@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed React Core Team Opened by a member of the React Core Team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants