-
Notifications
You must be signed in to change notification settings - Fork 558
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
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. |
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. |
Thanks for the update @gaearon ! |
Any update on this ? currently using this polyfill until this becomes native |
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. |
I recently asked about the status of the context selectors work over in the React 18 Working Group discussion forum: Per Andrew's comment:
|
@kristijorgji FWIW the This similar
So I'm guessing it may be a safer polyfill |
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 |
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