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 · 135 comments
Closed

Provide more ways to bail out inside Hooks #14110

gaearon opened this issue Nov 5, 2018 · 135 comments

Comments

@gaearon
Copy link
Member

@gaearon 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 #15156 (comment) for solutions to this.
@gaearon
Copy link
Member Author

@gaearon gaearon commented Nov 5, 2018

cc @markerikson you probably want to subscribe to this one

@markerikson
Copy link

@markerikson markerikson commented Nov 5, 2018

Yay! Thanks :)

@alexeyraspopov
Copy link
Contributor

@alexeyraspopov alexeyraspopov commented Nov 5, 2018

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
Member Author

@gaearon gaearon commented Nov 5, 2018

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

@markerikson
Copy link

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

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

@sophiebits sophiebits commented Nov 6, 2018

@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

@markerikson markerikson commented Nov 6, 2018

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

@dai-shi 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 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 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 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

@markerikson markerikson commented Nov 19, 2018

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

@FredyC
Copy link

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

@dantman
Copy link
Contributor

@dantman dantman commented Jan 30, 2019

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

@Jessidhia
Copy link
Contributor

@Jessidhia Jessidhia commented Jan 30, 2019

@olee
Copy link

@olee olee commented Jan 31, 2019

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.

@dantman
Copy link
Contributor

@dantman dantman commented Feb 1, 2019

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>;
}
@PedroBern
Copy link

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

@dai-shi 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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Linked pull requests

Successfully merging a pull request may close this issue.

None yet