Skip to content
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

Provide more ways to bail out inside Hooks #14110

Closed
gaearon opened this issue Nov 5, 2018 · 140 comments
Closed

Provide more ways to bail out inside Hooks #14110

gaearon opened this issue Nov 5, 2018 · 140 comments

Comments

@gaearon
Copy link
Collaborator

gaearon commented Nov 5, 2018

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.
@gaearon
Copy link
Collaborator Author

gaearon commented Nov 5, 2018

cc @markerikson you probably want to subscribe to this one

@markerikson
Copy link
Contributor

Yay! Thanks :)

@alexeyraspopov
Copy link
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?

@gaearon
Copy link
Collaborator Author

gaearon commented Nov 5, 2018

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

@markerikson
Copy link
Contributor

markerikson commented Nov 5, 2018

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

@sam-rad
Copy link

sam-rad commented Nov 5, 2018

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.

@markerikson
Copy link
Contributor

markerikson commented Nov 5, 2018

@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]);
}

@sophiebits
Copy link
Collaborator

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

@markerikson
Copy link
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?

@Jessidhia
Copy link
Contributor

Jessidhia commented Nov 8, 2018

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.

@ioss
Copy link
Contributor

ioss commented Nov 8, 2018

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?

@dai-shi
Copy link
Contributor

dai-shi commented Nov 15, 2018

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

@brunolemos
Copy link

brunolemos commented Nov 19, 2018

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

@slorber
Copy link
Contributor

slorber commented Nov 19, 2018

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

@gnapse
Copy link

gnapse commented Nov 19, 2018

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

@markerikson how official is this second argument? I see that it is not documented publicly yet.

I mention this because the api proposal mentioned by @sam-rad in this comment is what I was expecting it to be eventually, to solve the partial subscribing to context.

@markerikson
Copy link
Contributor

@slorber : See these links for more info on observedBits:

@gnapse : The React team has said a couple times that they're not sure they want to keep around the observedBits aspect, which is why the Context.Consumer's prop is still called unstable_observedBits.

@TotooriaHyperion
Copy link

TotooriaHyperion commented Nov 20, 2018

@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]);
}

Looks like useMemo can skip rendering wrapped in it, and the final update(by v-dom diff), but can't skip render function itself.

Personally,

I agree with using second argument as a selector with shallowEqual, since observedBits is a less common use case and selector can do what observedBits can do.
Also some times the needed data is a combination of nested context value and props in view hierachy, especially in normalized data structure with a nested view, when we want only to check the result of map[key], instead of the reference of map or the value of key, passing a selector can be very convenient:

function createSelectorWithProps(props) {
   return state => [state.map[props._id]];
}

function useContextWithProps(props) {
   return useContext(MyContext, createSelectorWithProps(props));
}

function ContextUsingComponent(props) {
    const [item] = useContextWithProps(props);
    
    // return ...........
}

But how to handle using multiple context?

 function ContextUsingComponent(props) {
     const [item] = useContextsWithProps(props, context1, context2, context3);
     
     // return ...........
 }

Finally the problem focuses on [rerender after calculate the data].
Thus I thought we need to useState with useObservable.
Observables trigger calculation, and shallowEqual the result, then set the result to local state.
Just the same as react-redux is doing, but with a hooks api style.

@danielkcz
Copy link

danielkcz commented Nov 20, 2018

I just found out about observedBits only thanks to @markerikson and somehow it felt like an awful solution. Working with bitmasks in JavaScript is not exactly something you see every day and imo devs are not really used to that concept much.

Besides, I find it rather awkward that I would need to kinda declare up front what my consumers might be interested in. What if there is a really an object (a.k.a store) with many properties and I would need each property to assign a bit number and most likely export some kind of map so a consumer can put a final bitmask together.

Well, in the end since I am a happy user of MobX, I don't care about this issue that much. Having an observable in the context, I can optimize rendering based on changes in that observable without any extra hassle of comparing stuff or having specific selectors. React won't probably introduce such a concept, but it could be one of the recommendations.

@TotooriaHyperion
Copy link

TotooriaHyperion commented Nov 22, 2018

how about this

// assuming createStore return a observable with some handler function or dispatch
function createMyContext() {
  const Context = React.createContext();
  function MyProvider({ children }) {
    const [store, setState] = useState(() => createStore());
    return <Context.Provider value={store}>{children}</Context.Provider>
  }
  return {
     Provider: MyProvider,
     Consumer: Context.Consumer,
     Context,
  }
}

const context1 = createMyContext();
const context2 = createMyContext();

 function calculateData(store1, store2) {
    //return something
 }

function ContextUsingComponent() {
  const store1 = useContext(context1.Context);
  const store2 = useContext(context2.Context);
  const [calculated, setCalculated] = useState(() => calculateData(store1, store2));
  function handleChange() {
     const next = calculateData(store1, store2);
     if (!shallowEqual(next, calculated)) {
        setCalculated(next);
     }
  }
  useEffect(() => {
     const sub1 = store1.subscribe(handleChange);
     const sub2 = store2.subscribe(handleChange);
     return () => {
        sub1.unsubscribe();
        sub2.unsubscribe();
     }
  }, [store1, store2])

  // use calculated to render something.
  // use store1.dispatch/store1.doSomething to update
}

@vijayst
Copy link

vijayst commented Nov 23, 2018

Without React NOT providing an option to cancel updates especially from context changes and because using useReducer hook causes various design constraints, we have to resort to good old Redux. Wrote an article based on developing a Redux clone based on existing API - context and hooks which explains more.

Two things are clear.

  1. Can't use Context API for global state unless we create a wrapper component (HOC, defeating purpose of hooks)
  2. No way to share state across container components when we use useReducer hook. Very rare that an app has independent container components for useReducer hook to be effective.

@TotooriaHyperion
Copy link

TotooriaHyperion commented Nov 23, 2018

@vijayst
I think this shouldn't be implemented as a "cancel" operation, it should be implemented as a way of "notice" instead.

Finally the problem focuses on [check if we need rerender after calculate the data].

This is exactly what react-redux do. And I wrote a example above to implement a react-redux like mechanism.
So let context to provide an observable is the convenient way to solve this problem. Rather than to "cancel" the update when we are already updating.

@PedroBern
Copy link

PedroBern commented Apr 16, 2019

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.

@dai-shi
Copy link
Contributor

dai-shi commented Jun 19, 2019

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

@alexburner
Copy link

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

@alexburner
Copy link

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
}

@priolo
Copy link

priolo commented Feb 8, 2022

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

@alexburner
Copy link

alexburner commented Feb 9, 2022

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

@priolo
Copy link

priolo commented Feb 9, 2022

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

@facebook facebook locked as resolved and limited conversation to collaborators Feb 9, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests