Skip to content

Provide more ways to bail out inside Hooks #14110

Description

@gaearon

There's a few separate issues but I wanted to file an issue to track them in general:

  • useState doesn'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.
  • useContext doesn'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.

Activity

  1. gaearon commented on Nov 5, 2018

    @gaearon
    ContributorAuthor

    cc @markerikson you probably want to subscribe to this one

  2. markerikson commented on Nov 5, 2018

    @markerikson
    Contributor

    Yay! Thanks :)

  3. alexeyraspopov commented on Nov 5, 2018

    @alexeyraspopov
    Contributor

    useContext doesn't let you subscribe to a part of the context value (or some memoized selector) without fully re-rendering.

    useContext receives observedBits as a second param. Isn't it the same?

  4. gaearon commented on Nov 5, 2018

    @gaearon
    ContributorAuthor

    I guess you're right the context one is an existing limitation (ignoring the unstable part).

  5. markerikson commented on Nov 5, 2018

    @markerikson
    Contributor

    @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!
    }

    observedBits is 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 observedBits in 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 to state.b results in bit 17 being turned on. In some connected component, we are interested in any changes to state.b, so we pass in a bitmask with bit 17 turned on. If there's only a change to state.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 if state.users[23] has changed.

  6. sam-rad commented on Nov 5, 2018

    @sam-rad

    Perhaps a possible api would be:

    function Blah() {
      // useContext(context, selectorFn);
      const val = useContext(Con, c => c.a.nested.value.down.deep);
    }

    And shallowEqual under the hood.

  7. markerikson commented on Nov 5, 2018

    @markerikson
    Contributor

    @snikobonyadrad: won't work - the second argument is already the observedBits value:

    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]);
    }
  8. sophiebits commented on Nov 6, 2018

    @sophiebits
    Contributor

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

  9. markerikson commented on Nov 6, 2018

    @markerikson
    Contributor

    Yeah, 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?

  10. Jessidhia commented on Nov 8, 2018

    @Jessidhia
    Contributor

    Crazy idea: add React.noop as 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.

  11. ioss commented on Nov 8, 2018

    @ioss
    Contributor

    I personally don't like noop, as I would expect it to do nothing. :)
    How about React.skipRerender or React.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?

  12. dai-shi commented on Nov 15, 2018

    @dai-shi
    Contributor

    Hi,
    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-redux

  13. brunolemos commented on Nov 19, 2018

    @brunolemos

    I 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 [], with result being the response of useContext(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

  14. slorber commented on Nov 19, 2018

    @slorber
    Contributor

    Hi,

    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 observedBits works and could be helpful here, anyone can explain? If we have to create an observedBits it 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)?

  15. 113 remaining items

  16. akalajzi commented on Jan 30, 2019

    @akalajzi

    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.

  17. gaearon commented on Jan 30, 2019

    @gaearon
    ContributorAuthor

    This 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!

  18. Jessidhia commented on Jan 30, 2019

    @Jessidhia
    Contributor

    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.

  19. dantman commented on Jan 30, 2019

    @dantman
    Contributor

    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).

  20. Jessidhia commented on Jan 30, 2019

    @Jessidhia
    Contributor
  21. olee commented on Jan 31, 2019

    @olee

    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.

  22. dantman commented on Feb 1, 2019

    @dantman
    Contributor

    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>;
    }
  23. PedroBern commented on Apr 16, 2019

    @PedroBern

    The 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.

  24. dai-shi commented on Jun 19, 2019

    @dai-shi
    Contributor

    Hi, 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

    This repo and that repo.


    (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

  25. alexburner commented on Sep 30, 2021

    @alexburner

    For anyone discovering this useContext() issue more recently (like me, today) here's an update on the current state of affairs AFAICT

    Problem

    • 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

    Current status

  26. alexburner commented on Nov 30, 2021

    @alexburner

    For 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
    }
  27. priolo commented on Feb 8, 2022

    @priolo

    I 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.jsx

  28. alexburner commented on Feb 9, 2022

    @alexburner

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

  29. priolo commented on Feb 9, 2022

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

  30. locked as resolved and limited conversation to collaborators on Feb 9, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions