RFC: Context selectors
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
User space implementation - https://github.com/dai-shi/use-context-selector
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.
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.
@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.
@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
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.
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
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.
@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
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>
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.
useIsolation would be wonderful. The memoization technique may result the same effect, but it would be difficult to create a custom hook, I suppose.
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.
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.”
@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.
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.
@mgenev : what "userland module" are you referring to?
@markerikson https://github.com/dai-shi/use-context-selector
I'm pretty convinced that the big problem we see today doesn't have to do with the propagation mechanism of the Context. The main problem that happens in practice is that we end up rendering beyond the component reading the Context because that component creates a new object (e.g. JSX element) that isn't referentially identical to the one before. So this would be solved if everything was auto-memoized.
However, since we're probably not going to have predictable auto-memoization in a reasonable time frame. It's probably better to introduce this as a stop-gap.
The nice thing is that this could be implemented either with the lazy propagation, or as a late bailout which would be closer to the final variant.
Regardless of implementation, in terms of API, I think we should implement Context selectors. I think we can just use the second argument to useContext and get rid of changed bits though.
The main problem that happens in practice is that we end up rendering beyond the component reading the Context because that component creates a new object (e.g. JSX element) that isn't referentially identical to the one before. So this would be solved if everything was auto-memoized.
Not sure I agree here. in v6 redux the HoC component would bail out by returning a memoized child if no update necessary. performance was unacceptable. when you compare that solution to this one i saw 10x improvement (random example had 40ms update time vs 4ms roughly)
the issue I think is that scheduling work for every connected component has a fixed cost and you get to avoid that by bailing out before scheduling in the first place
Also confused about your distinction between lazy prop and late bailout. lazy propagation is implemented via late bailouts. are you suggesting the auto-memoization is a late bailout?
Yeah, have to agree with Josh.
In RRv6, there were four major issues with perf:
- If I have N connected components, there are N wrapper components in the tree
- If a Redux action was dispatched, we immediately put that into
setState({storeState})in<Provider>at the root, which unconditionally forced a re-render even if 0 components were actually interested in the change, and that change was always starting at the root.<Provider>memoized its children, so the render didn't completely cascade from the root, but React still had to traverse the tree to find all context consumers. - All N connected wrappers would have to re-render in order to run
mapStateand calculate if the wrapped child needed to re-render - All that
mapStatecalculation was done synchronously while rendering the wrappers
In contrast, v7 and connect still have N wrapper components, but if only say 5 out of 100 components actually have new values returned from mapState, only those 5 will have renders queued, and they are much lower in the tree. So, while I know React always starts renders from the root, it can skip over most of the tree and just update the few sub-trees that have renders queued.
So, huge differences in if renders are queued, and how many components are flagged for updates, and doing less work is generally going to be faster.
With context selectors, we might still be facing the "unconditional render starting at the root" and "calculating selectors while rendering" aspects. It's possible that useMutableSource may allow us to work around that. But, at least we wouldn't be forcing every connected component to fully re-render.
(and as I've said, it's also entirely possible that this is only something we can implement with useSelector and not connect.)
A couple of base line things to consider here before we continue:
-
Previous attempts were measuring a React implementation that isn't optimized for this case. We're doing work to simplifying a lot of these steps as the research has settled on which Concurrent Mode features we actually need compared to what was theoretical. A few things we can do is for example change the render loop when a parent bails out to search down the tree, find work and only lazily clone the parent tree if these actually cause new work. This would help the setState case in the same way as context traversal would. Because right now you could end up traversing down a tree deeply only to find out that the setState didn't lead to a new change. This has the same negative effects so it would be better to solve this generically rather than specifically for Context. This also helps save bytes.
-
Redux has a different use case than useReducer + Context. Redux has more work to do before entering React but some of that work is similar to what React has to do. So there's some duplication. It's also possible to avoid the call. For useReducer you need to enter React's render, and that's important for the semantics of the priorities. So I prefer to compare the useReducer + Context performance to itself rather than comparing an early bailout in Redux.
-
When we talk numbers it's very important to talk about the improvement to lower bounds and upper bounds. Improving a very short update is never going to be a priority if that makes large updates slower. Going from 1 second to 10 second is absolutely unacceptable but going from 1ms to 10ms is perfectly fine. So making rerenders that affect a small part of the tree faster than what is observable isn't a goal, but making medium size updates that are observable are important.
-
I suspect that a lot of these tradeoffs play out differently in sync mode vs concurrent mode. In sync mode you may end up with a lot of scans due to a single sequence of events having to do one render per event where as in concurrent mode they would be batched into a single render and therefore have fewer passes. E.g. a 40ms pass could easily turn into a 400ms pass where as it would be 40ms in concurrent mode. So the tradeoffs are different. You might see more things actually updating in a single render pass as a result so you end up wasting less.
That said, I think a lot of the lazy optimizations make sense but not just for context but the whole algorithm. There's a lot of value in sharing this.
@sebmarkbage are there established methods to test upper and lower bounds across sync and concurrent modes? I found this part of testing #118 very hard because I had to contrive some stress tests but they were all completely loaded by context updates. If you add in other work it drowns out the fundamental performance changes (context was always pretty fast) and if you leave it out you don’t represent what any real world application would actually experience. I ended up using it in a real application with some performance benchmarking but then I’m comparing aggregate performance so harder to do apples to apples comparisons
Related to the technique applying to useState I have a pretty clear picture of what might work. I’ll see if I can put together a poc
Hello! I remembered this thread and decided to pass by and share an alternative to use-context-selector I've been working on:
There are a couple of differences I'd like to point out:
- Keeps track of listeners by Context, instead of a single Set of listeners globally.
- Accepts providing your own
equalityFnto compare the context (by default performs shallow comparison). - Provides a custom
Context.Consumerwhich accepts selecting from the context too. - Runs the same logic on
developmentandproductionmode; in production-modeuse-context-selectortriggers re-renders while in render phase (which is not-allowed in React and would warn in console on development-mode).
Linking to useMutableSource(), as it seems to aim at solving this problem officially, and there was no link posted here so far:
https://github.com/reactjs/rfcs/blob/master/text/0147-use-mutable-source.md
I know @adrian-marcelo-gallardo is already aware of it from my code comment, but let me note here that I've already implemented a userland implementation with useMutableSource. https://github.com/dai-shi/use-context-selector/pull/12 It's published with the alpha tag.
I created a toolkit for managing state using ContextAPI. It provides useSelector (with autocomplete) as well as useDispatch.
It uses:
- use-context-selector to avoid unneccesary rerenders.
createSlicefrom @reduxjs/toolkit to make the state more modular and to avoid boilerplate.
The library is available here:
Anyone knows if there's any progress update regarding this feature of selectors?
@slorber
Linking to useMutableSource(), as it seems to aim at solving this problem officially, and there was no link posted here so far:
It does not attempt to solve the same problem. It's more of an escape hatch.
Anyone knows if there's any progress update regarding this feature of selectors?
This is a multi-month project to implement because it would require significant architectural changes. It's on our mind but there's unlikely to be any progress updates in the very near term.
@gaearon Thanks Dan 👍
@gaearon
It does not attempt to solve the same problem. It's more of an escape hatch.
Not sure to understand what you mean here.
https://github.com/reactjs/rfcs/blob/master/text/0147-use-mutable-source.md#redux-stores
The RFC shows an example of implementing useSelector on top of useMutableSource. This looks to me exactly what users in this issue want: a system to avoid triggering unnecessary re-renders when your big state changes.
How can this be only considered an escape hatch? Do you plan another better solution to this problem in the future?
Maybe we could put the state directly in the context, and then have an official useSelector hook and core implementation, that could have a few other benefits (maybe related to zombie childrens), but for the end user and simple usecases that looks quite similar to me.
@slorber : useMutableSource and "context selectors" solve two very different use cases:
useMutableSourceis about allowing non-React data sources to better integrate with Concurrent Mode to avoid tearing. It does this by checking for changes in the underlying data source, and restarting the render pass if any changes are detect mid-render. That behavior is less than ideal from React's perspective, hence the "escape hatch".- "Context selectors" are about giving components a way to selectively re-render by only extracting part of the data contained in a given context value, with the presumption that the data being used to construct the context value is likely all being kept directly in React.