Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RFC: Context selectors #119

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open

RFC: Context selectors #119

wants to merge 2 commits into from

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

@theKashey theKashey commented Jul 8, 2019

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

Loading

@dantman
Copy link

@dantman 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.

Loading

@j-f1
Copy link

@j-f1 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.

Loading

@dantman
Copy link

@dantman 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.

Loading

@gnoff
Copy link
Contributor Author

@gnoff 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

Loading

@PedroBern
Copy link

@PedroBern 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.

Loading

@gnoff
Copy link
Contributor Author

@gnoff 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

Loading

@GasimGasimzada
Copy link

@GasimGasimzada GasimGasimzada commented Jan 4, 2020

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>

Loading

@sebmarkbage
Copy link
Collaborator

@sebmarkbage 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.

Loading

@dai-shi
Copy link

@dai-shi 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.

Loading

@theKashey
Copy link

@theKashey theKashey commented Jan 6, 2020

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.

Loading

@GasimGasimzada
Copy link

@GasimGasimzada GasimGasimzada commented Jan 6, 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.”

Loading

@gnoff
Copy link
Contributor Author

@gnoff 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.

Loading

@mgenev
Copy link

@mgenev 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.

Loading

acdlite added a commit to acdlite/react that referenced this issue Feb 26, 2021
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 = useSelectedContext(Context, c => select(c));
```

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.)
acdlite added a commit to acdlite/react that referenced this issue Feb 26, 2021
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.)
acdlite added a commit to acdlite/react that referenced this issue Feb 27, 2021
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.)
@ntucker
Copy link

@ntucker ntucker commented Feb 27, 2021

This is about facebook/react#20646

First off, I really like this direction, so thanks for all the work on this.

Does this work with variable number of items selected? i.e.,

const fullContext = useContextSelector(Context, context => context.a.map(k => context[k]));

This could be helpful for cases of dynamic (based on state rather than known statically) select like normalization.

Loading

@joepuzzo
Copy link

@joepuzzo joepuzzo commented Apr 13, 2021

@gnoff Is there a list of reasons this has not been implemented in core react yet? what are the major blockers?

I ask this because IMO, this seems to be one of the more important issues that react has at the moment. I see more developers starting to use the context api every day, and don't appreciate the pitfalls that it comes with in terms of rendering.

Context selectors seem like a simple win for react. There are many different userland implementations to consider, but they have proven to work and be simple to build.

The current context api makes it really easy for users to write inefficient react code.

Loading

@markerikson
Copy link

@markerikson markerikson commented Apr 13, 2021

Loading

@rhyek
Copy link

@rhyek rhyek commented Apr 21, 2021

Has there been a consideration for providing an equality function as an option to the selector similar to what you can do with redux's useSelector (https://react-redux.js.org/api/hooks#equality-comparisons-and-updates)?

This is mostly about ergonomics, but it would be great to be able to specify a shallow equality comparator for some use cases. It is particularly interesting to be able to select multiple values from a context at once without needing to spread that out using multiple hook statements to work around the referential equality issue (Object.is).

Loading

@LXSMNSYC
Copy link

@LXSMNSYC LXSMNSYC commented Apr 30, 2021

@rhyek there was a discussion about adding a third argument for useContext/useContextSelector that allows filtering updates much like useMutableSource. Not sure if it has advanced yet.

Loading

acdlite added a commit to acdlite/react that referenced this issue Jul 7, 2021
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.)
acdlite added a commit to acdlite/react that referenced this issue Jul 7, 2021
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.)
acdlite added a commit to acdlite/react that referenced this issue Jul 7, 2021
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.)
acdlite added a commit to acdlite/react that referenced this issue Jul 8, 2021
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.)
acdlite added a commit to acdlite/react that referenced this issue Jul 8, 2021
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.)
acdlite added a commit to acdlite/react that referenced this issue Jul 10, 2021
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.)
@gaearon
Copy link
Member

@gaearon gaearon commented Aug 18, 2021

In the spirit of #182, a small update. We've experimented with facebook/react#20646 (inspired by this proposal, but with some API tweaks). We saw some perf improvements but more experimentation will be needed. We don't have other updates at this moment.

Loading

@ntucker
Copy link

@ntucker ntucker commented Aug 18, 2021

Thanks for the update @gaearon !

Loading

@kristijorgji
Copy link

@kristijorgji kristijorgji commented Sep 8, 2021

Any update on this ?

currently using this polyfill until this becomes native
https://www.npmjs.com/package/@fluentui/react-context-selector

Loading

@gaearon
Copy link
Member

@gaearon gaearon commented Sep 8, 2021

If there was an update, it would have been posted. :-) Please don’t leave comments like “is there an update” because they only create notification noise for others.

Loading

@markerikson
Copy link

@markerikson markerikson commented Sep 8, 2021

I recently asked about the status of the context selectors work over in the React 18 Working Group discussion forum:

reactwg/react-18#73

Per Andrew's comment:

We ran an experiment of the lazy propagation mechanism that showed mildly positive performance results, but we haven't run an experiment for context selectors yet. We're not really blocked by anything, it just hasn't been a priority while we work on other 18-related projects.

Loading

@alexburner
Copy link

@alexburner alexburner commented Sep 30, 2021

@kristijorgji FWIW the react-context-selector library you linked "uses undocumented feature of calculateChangedBits" https://github.com/microsoft/fluentui/tree/master/packages/react-context-selector#technical-memo

This similar use-context-selector library (listed as inspiration to the above) was updated to avoid that:

Prior to v1.3, it uses changedBits=0 feature to stop propagation, v1.3 no longer depends on this undocumented feature.

So I'm guessing it may be a safer polyfill

Loading

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

Successfully merging this pull request may close these issues.

None yet