-
Notifications
You must be signed in to change notification settings - Fork 565
RFC: Context selectors #119
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
base: main
Are you sure you want to change the base?
Conversation
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 // 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 |
You could work around the issue with |
@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 |
@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 It's almost more like a |
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 reducerexport const reducer = (state, action) => {
...
}; Create your ContextProvider componentexport 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
Write components as pure functional componentThis 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 HOCfunction connectToContext(WrappedComponent, select){
return function(props){
const selectors = select();
return <WrappedComponent {...selectors} {...props}/>
}
} Put it all togetherimport 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 questionThe re-render avoided
Other solutionsI 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, |
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 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. |
|
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. |
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.” |
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
This is super cool. it's like 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. interestingly there may be a way to combine this with |
Personally, it does not confuse me. It makes everything much better and I'm adopting the userland module for it already. |
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.
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.
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.
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.
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.
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.
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.
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.
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.
Is this going to be the solution for React 19?
|
This won't be supported in the initial release of React 19 it seems: Also I think they're looking to optimize out useMemo: 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 |
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 |
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 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. 😅 |
+1 This would be huge. |
@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 " |
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 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. |
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
Addendum
Example: https://codesandbox.io/s/react-context-selectors-xzj5v
Implementation: https://github.com/gnoff/react/pull/3/files