Repository navigation
Provide more ways to bail out inside Hooks #14110
Description
Activity
cc @markerikson you probably want to subscribe to this one
Reacted by Kai Hao and lsnchYay! Thanks :)
Reacted by Emrah Bilbay, Adrian Pascu, Stepan Kuzmin, Jack Ford, Niklas Wagner, Moshe Brevda, Heaven, Martin V., Ben Anderson, iamcastelli and 4 moreuseContextdoesn't let you subscribe to a part of the context value (or some memoized selector) without fully re-rendering.useContextreceivesobservedBitsas a second param. Isn't it the same?I guess you're right the context one is an existing limitation (ignoring the unstable part).
@alexeyraspopov : nope! Here's an example:
function ContextUsingComponent() { // Subscribes to _any_ update of the context value object const {largeData} = useContext(MyContext); // This value may or may not have actually changed const derivedData = deriveSomeData(largeData); // If it _didn't_ change, we'd like to bail out, but too late - we're rendering anyway! }
observedBitsis for doing an early bailout without actually re-rendering, which means you can't locally do the derivation to see if it changed.As an example, assuming we had some magic usage of
observedBitsin React-Redux:Imagine our Redux state tree looks like
{a, b, c, d}. At the top, we calculate bits based on the key names - maybe any change tostate.bresults in bit 17 being turned on. In some connected component, we are interested in any changes tostate.b, so we pass in a bitmask with bit 17 turned on. If there's only a change tostate.a, which sets some other bit, React will not kick off a re-render for this component, because the bitmasks don't overlap.However, while the component is interested in changes to bit 17, it still may not want to re-render - it all depends on whether the derivation has changed.
More realistic example: a user list item is interested in changes to
state.users, but only wants to re-render ifstate.users[23]has changed.Reacted by Bruno Lemos, Karolis Narkevicius, Junyoung/"Clare" Jang, Valdis Veidelis, Peng Xiao and RopitalPerhaps a possible api would be:
function Blah() { // useContext(context, selectorFn); const val = useContext(Con, c => c.a.nested.value.down.deep); }
And
shallowEqualunder the hood.Reacted by Mike Simmonds, Elaina Cherudim, Ivan Lysov, —, Dimitry, ChenYong, Mo Doaie, Вадим Бударин, 衍良, Maxim Syabro and 7 more@snikobonyadrad: won't work - the second argument is already the
observedBitsvalue:export function useContext<T>( Context: ReactContext<T>, observedBits: number | boolean | void, )
@gaearon : semi-stupid question. Given that returning the exact same elements by reference already skips re-rendering children, would
useMemo()kinda already solve this?function ContextUsingComponent() { const {largeData} = useContext(MyContext); const derivedData = deriveSomeData(largeData); const children = useMemo(() => { return <div>{derivedData.someText}</div> }, [derivedData]); }
Reacted by Emrah Bilbay, Vinicius Pacheco, Elaina Cherudim, Hao Jiayin, Henri Beck, Adam Rackis, Jeswin, Sandrina Pereira, kiho, Pablo Lee and 2 more@markerikson Yes, but that means that ContextUsingComponent needs to know about this, even if you might otherwise want to put the two useContext+derive calls into a custom Hook.
Reacted by Chris Cowan and Elaina CherudimYeah, I know, just tossing it out there as a sort of semi-stopgap idea.
Any initial thoughts to what a real API like this might look like?
Reacted by Brandon and Emrah BilbayCrazy idea: add
React.noopas a reconciler-known symbol,throw React.noop;Not sure how that would mesh with this interrupting further hooks from running, and there is already a problem with the reconciler throwing out hooks that did already run before a component suspends.
Reacted by Debove Christopher, Apostolos Tsakpinis, —, Biel Simon Rojas, Luis Felipe Zaguini, Kostya Kurilov, Len and Alex WatsonReacted by Ruslan @doasync and Daniel FriesenI personally don't like
noop, as I would expect it to do nothing. :)
How aboutReact.skipRerenderorReact.shouldComponentUpdate(() => boolean | boolean)or similar?Also, it should be a call:
React.whateverName(), which could then do whatever is needed (probably throw a marker, as you suggested) and especially ignore the call on the first render, which should probably not be skipped.I also thought about the possibility to return early in the render method with a marker (
React.SKIP_UPDATE), but that wouldn't work in custom hooks. On the other hand, skipping rerendering in custom hooks might be strange? What do you think?Reacted by LenHi,
I'm experimenting a new binding of Redux for React.
For now, I use a workaround for bailing out.
Does the scope of this issue cover this use case?
https://github.com/dai-shi/react-hooks-easy-reduxI would enjoy something like this to avoid unnecessary re-renders:
const { data } = useContext(MyContext, result => [result.data])
where the second parameter would work like the second parameter of
useEffect, except it's() => []instead of[], withresultbeing the response ofuseContext(MyContext).Note: This is supposing the existing second param could change or react internally could check the typeof to see if it's a function or the observedBits
Reacted by Heliton Nordt, Isac, Vijay Thirugnanam, Emrah Bilbay, Esa-Matti Suuronen, Ernesto García, alid-code-archives, Yuan (Bob) Gong, Margarita Krutikova, Aaron Schubert and 56 moreReacted by Dinislam Maushov, Benjamin A Pusey, Jules Sam. Randolph, Ezran Bayantemur and Esdras JúniorReacted by JohnReacted by Kevin, Matt Zygmunt, Adam Pietrasiak, Casey Baggz, Dani Akash, Dinislam Maushov, Jules Sam. Randolph, Xuan Huang (黄玄) and Ezran BayantemurReacted by James Gillmore, Johnny Peter, Matt Zygmunt, Casey Baggz, Dani Akash, Dinislam Maushov, William, Christos Lytras, Benjamin A Pusey, Jules Sam. Randolph and 1 moreHi,
I would also like the same api as @brunolemos describes, for using it with tools like Unstated which I use as a replacement for Redux store with a similar connect() hoc currently.
But I think there is ambiguity in your API proposal @brunolemos not sure exactly what happens if you return [result.data, result.data2] for example. If you return an array you should probably assign an array to the useContext result too?
Not sure exactly how
observedBitsworks and could be helpful here, anyone can explain? If we have to create anobservedBitsit should probably be derived from the context value data slice we want to read no? so current api does not seen to do the job for this usecase.What if we could do
const contextValue = useContext(MyContext, generateObservedBitsFromValue)?113 remaining items
As someone who has hit this point with
useContext, it becomes a pointless hook, and as @dantman mentions, simply using .Consumer (or HOC) makes more sense then useContext -> another component to memo. So currently in a project, we've been using useContext on all small-ish parts, and had to resort to HOCs where we had to prevent unnecessary updates.
Its doable, of course, as there's always that fallback, but its hardly a great solution when we went with hooks to actually avoid HOC pattern.Reacted by Alex BurnerThis discussion will be more productive if we have an actual code example we can discuss.
That said this discussion is also going in circles. I’m not sure it’s helpful to keep reiterating the same points.
This is a “nice to have” request but it ties into many other questions around context. We won’t likely address it in isolation but we can revisit it in about half a year and see where we are.
Ideally there could be a follow up issue focused on this particular request that also has a realistic demo that shows the problematic use case suffering from the performance problem. That would anchor the discussion. Thanks!
From some experiments I've done this week on actual benchmarkable code, it doesn't seem that providing a "shouldUpdate" callback to useContext / context.Consumer does it any good.
Granted, I emulated it by going around the context API and using a Set to store subscriptions instead (the Context only held the never-updating subscribe/unsubscribe logic). But currently, as long as updates have to be sequentially consistent, the cost with or without a "shouldUpdate" check is about the same, or higher with the extra check (assuming children are memoized).
I can probably write about it in more detail elsewhere.
But currently, as long as updates have to be sequentially consistent, the cost with or without a "shouldUpdate" check is about the same, or higher with the extra check (assuming children are memoized).
This sounds like you are effectively testing memoizing once (memoizing the result) and memoizing twice (SCU/React.memo style memoization of useContext then memoizing the result). Which of course generally won't improve performance much, unless there are special cases where the component does semi-expensive computation outside of the memoization and the context is frequently updated in ways that SCU would stop the component being updated.
Assuming children uses React.memo only works if the useContext is not being used in a moderately sized tree with nesting (components with children are difficult/messy to memoize). And using something like useMemo to memoize generation of the whole tree "works", but is a mess that IMHO is undesirable (you're effectively re-implementing in user-land what React.memo should be doing).
- This was specifically in an attempt to get sCU / React.memo to intervene before updating children in a redux store.
Isn't the general case just that we need a useContext that allows us to select just a part of the context and only update, if that returned part was changed?
This was already proposed above like here: #14110 (comment)
However after it was said that the second argument is already used for observed_bits, it was somehow not discussed much any more as far as I can see.But what about just adding it as an overload that checks if the second argument is a function and then uses that to select parts out of a context
function MyContextComponent(props) { const selectedCtxValue = useContext(MyContext, ctx => ({ value1: ctx.sub1.sub1.value, value2: ctx.sub2.sub1.value, })); }
Something like this which should only update if the returned update fails a shallow equal test?
I didn't see any definite argument against allowing such a feature.My solution I was thinking of making an RFC for would be
MyContext.slice(value => value.foo). Which would work for any type of context usage.Static
const EmailContext = UserContext.slice(user => user.email); exports default MyComponent() { const email = useContext(EmailContext)l // ... }
Dynamic with hooks:
const KeyContext = useMemo(() => MyContext.slice(value => value[key]), [key]) const keyValue = useContext(KeyContext);
Hooks + Consumer:
const KeyContext = useMemo(() => MyContext.slice(value => value[key]), [key]) return <KeyContext.Consumer>{keyValue => keyValue}</KeyContext.Consumer>
Classes:
@decorate(memoizeOne) getKeyContext(key) { return MyContext.slice(value => value[key]); } render() { const {key} = this.props; const KeyContext = this.getKeyContext(); return <KeyContext.Consumer>{keyValue => keyValue}</KeyContext.Consumer>; }
Reacted by Luke Lindsey, Michał Skowronek, Aaron, Matt Kaniaris, Álvaro Cuesta, Ivan Lysov, Dave Jeffery, Aarjith Nandakumar, Gustav Bylund, Kasper Pihl and 26 moreReacted by Maxim SyabroThe way im doing:
Connect
function connect(WrappedComponent, select){ return function(props){ const selectors = select(); return <WrappedComponent {...selectors} {...props}/> } }Select
function select(){ const { mySelector, otherSelector } = useContext(AppContext); return { mySelector: mySelector, otherSelector: otherSelector, } }Pure Functional Component with React.memo
const MyComponent = React.memo(({ mySelector, otherSelector, someRegularProp }) => { ... }); export default connect(MyComponent, select)Usage
<MyComponent someRegularProp={something} />Conclusion
The select function get computed every time the context update, but since it does nothing, its cheap, all the logic must go inside the pure component, that does not get re-render.
Reacted by Tyler Waters, pedrobern, Giancarlo Soverini, Mario Jerkovic, Jesse Lee, Kovalchuk Yuriy, David Alberto Adler and YuriHi, I'd like to share my workaround.
Pseudo code
const calculateChangedBits = () => 0; const MyContext = React.createContext(null, calculateChangedBits); const useMyContext = () => { const value = React.useContext(MyContext); const forceUpdate = useForceUpdate(); // and use subscription to detect changes and force update. };
Concrete examples
(edit) Just noticed it's already suggested half a year ago. #14110 (comment)
E.g. setting changed bits to zero and then manually force updating the relevant children.
(edit2) Here's a more simplified library. use-context-selector
Reacted by Erfan MirzapourFor anyone discovering this
useContext()issue more recently (like me, today) here's an update on the current state of affairs AFAICTProblem
useContext()is currently only recommended for values that don't change very often ("very low read overhead, at the cost of more expensive updates" — comment here)- The two main performance hits are:
- It doesn't let you subscribe to a part of the context value (or some memoized selector) without fully re-rendering (comment here)
- The whole provider tree is traversed for context consumers on each update, there isn't a "remembered" list of subscribers (comment here)
Solutions
- The three original mitigation options are outlined here: Preventing rerenders with React.memo and useContext hook. #15156 (comment)
- @dai-shi wrote a nice blog post about them here and added a Proxy-based library as a fourth option
- @gnoff opened up RFC#119 for context selectors here RFC: Context selectors reactjs/rfcs#119
- @acdlite created an experiment based on that RFC here [Experiment] Context Selectors #20646
- Microsoft Fluent UI and @dai-shi both provide userland versions of these proposals:
Current status
- Context Selectors are currently taking a backseat, while React 18 is released. Conversation here Context selector status and plans? reactwg/react-18#73
Reacted by Mark Erikson, itsMapleLeaf, Jin Yao, Matej Bransky, Verekia, Vinicius, Hassam Ali, Somto Medua-Ugeh, Brandon Lorenz, aaa and 5 moreFor posterity, we ended up going with @dai-shi's
useContextSelector()and are still happy with the result❌ Before
All components re-render for any context property change:
interface MyContext { items: Item[] selectedIndex: number pagination: PaginationStuff sorting: SortingStuff filtering: FilteringStuff searching: SearchingStuff } const ComponentA = () => { const { items, selectedIndex } = useContext(MyContext) return // ...etc } const ComponentB = () => { const { filtering, searching } = useContext(MyContext) return // ...etc } const ComponentC = () => { const { pagination, sorting } = useContext(MyContext) return // ...etc }
✅ After
Each component only re-renders for properties it cares about:
const ComponentA = () => { const items = useContextSelector(MyContext, (v) => v.items) const selectedIndex = useContextSelector(MyContext, (v) => v.selectedIndex) return // ...etc } const ComponentB = () => { const filtering = useContextSelector(MyContext, (v) => v.filtering) const searching = useContextSelector(MyContext, (v) => v.searching) return // ...etc } const ComponentC = () => { const pagination = useContextSelector(MyContext, (v) => v.pagination) const sorting = useContextSelector(MyContext, (v) => v.sorting) return // ...etc }
Reacted by Adam Rackis, Ernesto García, Giacomo Rebonato, Jaime Menéndez and Lure5134Reacted by Adam Rackis, Jaime Menéndez and Lure5134I haven't read ALL the discussion but
can't be solved with "useMemo" on the component?
I use the JON library here but the concept is the same:
https://codesandbox.io/s/test-render-memo-47rt7?file=/src/Cmp2.jsxReacted by Meligy@priolo that's one of the three original mitigation options (#15156 (comment)), which are linked to in the first comment of this thread: #14110 (comment)
Reacted by priolo@alexburner you're right, I'm sorry
is that this link gave me one to corroborate his "component-centric" thesis of React by focusing on a specific comment and I was hasty in commenting!- locked as resolved and limited conversation to collaborators
on Feb 9, 2022
There's a few separate issues but I wanted to file an issue to track them in general:
useStatedoesn't offer a way to bail out of rendering once an update is being processed. This gets a bit weird because we actually process updates during the rendering phase. So we're already rendering. But we could offer a way to bail on children. Edit: we now do bail out on rendering children if the next state is identical.useContextdoesn't let you subscribe to a part of the context value (or some memoized selector) without fully re-rendering. Edit: see Preventing rerenders with React.memo and useContext hook. #15156 (comment) for solutions to this.