facebook/react

Provide more ways to bail out inside Hooks

gaearon opened this issue Β· 140 comments

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.

cc @markerikson you probably want to subscribe to this one

Yay! Thanks :)

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?

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

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.

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.

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

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

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?

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 commented

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?

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

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

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

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.

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

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

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.

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
}

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.

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

Hello! I personally don't like the idea of providing a way to cancel a render that is alredy being performed because it make the behavior of the component non-linear and much more difficult to understand. Even more this aggravated by the fact that the cancellation may be performed inside a custom hook and will be hidden from your eyes.

The better solution, in my opinion, is to provide a way to subscribe to only what you exactly need.

How about to add subscribe and unsubscribe methods to the Context instance? Then useContext can be implemented like this:

const useContext = (Context, getDerivedValue) => {
  const [state, setState] = useState();

  const handleContextChange = (value) => {
    const derived = getDerivedValue(value);

    if (derived !== state) { // do not perform setState without need
      setState(derived);
    }
  }

  useEffect(() => {
    Context.subscribe(handleContextChange);
    return () => Context.unsubscribe(handleContextChange);
  }, []);

  return state;
};

Usage:

const Context = React.createContext();

const Component = () => {
  const value = useContext(Context, ctx => ctx.a.b.c.deep);
};

What about useState, it can check the strict equality of a previos and new values, and bailout the update. It should be enough to be able to do what we can do in a class components.

I like @strayiker approach to useContext hook rather than bailing out. I am going to check if it works in my Redux clone - I already have a useStore hook which wants to do something like that.

@strayiker So, i wrote an useStore hook (most likely how useRedux will be implemented later). Instead of subscribing / unsubscribing to Context (api does not exist now), I have some custom store helpers (like redux).

import { useEffect, useState } from 'react';
import { subscribe, unsubscribe } from './storeHelpers';
import shallowEqual from './shallowEqual';

let oldState;

export default function useStore(mapContextToState, initialState) {
    const [state, setState] = useState(initialState);
    oldState = state;

    useEffect(() => {
        subscribe(handleContextChange);
        return () => unsubscribe(handleContextChange);
    }, []);

    const handleContextChange = (context) => {
        if (typeof mapContextToState === 'function') {
            const newState = mapContextToState(context);
            if (!shallowEqual(newState, oldState)) {
                setState(newState);
            }
        } else {
            setState(context);
        }
    }

    return state;
}

Another solution it's providing a way to ignore updates from nested hooks. May be something like another hook useShouldUpdate(hook, shouldUpdate). It can be used to avoid unnecessary updates when context/state is changing.

// Non-optimal hook that change state too often and subscribes to big context updates
const useHook = () => {
  const [state, setState] = useState(0); // frequently changed state
  const context = useContext(Context); // big context with many unnecessary fields, that changes too often
  
  useEffect(() => {
    const t = setInterval(() => setState(s => s + 1), 100); // 10 times per second + 1
    return () => clearInterval(t);
  }, []);

  return {
    state: parseInt(state / 10), // resulting state will be changed 1 time for 10 updates, but component will be re-rendered each time.
    slowValue: context.slowValue,
  };
};

// We use `useShouldUpdate` to bail out some of state and context changes 
const useDebouncedHook = () =>
  // Forces the component to ignore updates until the second argument will return true
  useShouldUpdate(useHook, (result, prevResult) => {
    return !shalowEqual(result, prevResult);
  });

Usage

const Component = () => {
  // will trigger re-render when state is multiple of 10 or slowValue was changed
  const { state, slowValue } = useDebouncedHook();
  
  return (
    <div>
      <div>{state}</div>
      <div>{slowValue}</div>
    </div>
  )
}

diagram

What does useShouldUpdate do that causes the current render to be cancelled? Whatever it does, is what @markerikson needs access to for Redux.

However much you might think this increases complexity, there are numerous use cases where this would help a great deal.

Would the ultimate solution for this, and issues around people drawing a mental picture of how the hooks run in every render, be to find a way to separate hooks from render somehow?

The component that uses hooks can return the render function. setState could return an object with get and set, and this object will be in scope of the render function:

export default HelloWorld = React.useHooks(props => {
  const name = setState('world');
  return () =>
    <div>Hello {name.get()}</div>;
});

@arackaf : the point of this thread is that there is currently no way to bail out of the process of rendering a function component once it's started, and that that capability is needed before hooks can probably be finalized.

@Meligy : variations on that approach were suggested several times in the hooks RFC thread, and based on Sebastian's comments, I don't think they're going to alter the basic form of hooks to use that.

@markerikson indeed - I may have been unclear. I was responding more to comments along the line of

I personally don't like the idea of providing a way to cancel a render that is alredy being performed because it make the behavior of the component non-linear and much more difficult to understand

followed by suggestions for a useShouldUpdate hook which would seemingly do the same thing.

Ok. Sorry, I only caught up with most recent comments in RFC issue. And the approach sounded like a clear answer to "already rendering" issue because it would remove that situation completely. I fully respect if the people who came up with the idea and have way more context than me decided it's too late and/or a bad idea to alter the approach.

I'd like to read thoughts about this, specially from the core team

@arackaf Not exactly the same. Suggested useShouldUpdate allow to avoid unnecessary rendering at all rather than cancel it when it's already performing.

Although this is kind of re-posting, I would like to understand what "bail out" means here (especially useState). Isn't it like "early return" in rendering?

I'm experimenting with a workaround using Exception and ErrorBoundary like this: https://github.com/dai-shi/react-hooks-easy-redux/blob/093003dc40905fa9ac80dd57777e2cd441c60b75/src/index.js#L87-L100
It can be too confusing as it breaks hooks rules.

@dai-shi : ultimately, the goal is an equivalent of shouldComponentUpdate for function components. But, since a function component is just a function, and calling that function is re-rendering, then the function component has to start rendering in order to do anything.

@markerikson Thanks for the explanation.

What I could think of is:

import React, { KeepPreviousElement } from 'react';

const MyComponent = () => {
  const [count, setCount] = useState(0);
  const prevCount = usePrevious(count);
  const shouldUpdate = count > prevCount; // or whatever...
  if (!shouldUpdate) return <KeepPreviousElement />;
  return <div>{count}</div>;
};

This doesn't allow bailing out silently in a custom hook, though.


I also like @strayiker's useShouldUpdate style, but if I'd go for it, useShouldUpdate could be called several times in different custom hooks, and I'm not sure the behavior in this case (whether they are combined as logical AND or OR.)

Here the POC of the useShouldUpdate that realized in user space with some hacks Β―\_(ツ)_/Β―

Edit xo92v49yyz
Open the console to see what happenning

Nice!
Though, ideally, I'd expect a hook to be a simple fuction like:

const useGoodHook = delimiter => {
  const value = useBadHook(delimiter);
  const prev = usePrevious(value);
  useShouldUpdate((value, prev) => value !== prev, [value, prev]);
  return value;
};

Otherwise, we should call it differently from hooks (not use* convention). For example:

const useGoodHook = applyShouldUpdate(useBadHook, (value, prev) => value !== prev);

Actually, yes, it should be called differently from hooks because it's not a hook.

BTW, my original proposition is something like this:

import React, { BailOutRendering } from 'react';

const useGoodHook = delimiter => {
  const value = useBadHook(delimiter);
  const prev = usePrevious(value);
  if (value !== prev) throw new BailOutRendering();
  return value;
};

(This might be too magical, though.)

The userland implementation of this is here and the codesandbox is here.

Why not just return "undefined" to bail out?
Returning "null" already renders nothing "undefined" would just rerender the same thing.

I understand that hooks have to be called in the exact same order but it also seams that as long as you keep that order you can return early. Is it so?

Basically there would be no need for a context selector, just use the render function.

With just a simple hook to compare last values and a return undefined would do the trick.

I createad an example in sandBox.
Edit pw9nnx3k20

"withBail" is a HOC that injects a bail function that returns last rendered value.
The "useShouldUpdate" (that actually should be name "useShallowEqual") just shallow compares current and last values.

The App component provides the context and passes a prop to "MyComp"
A counter and remaiders are used to change context data and prop at different times.

The changes to state are even done in useMemo not useEffect. Would this be a problem?

Seams simple enough.
I'm I missing something?

@ruifortes : I don't think return undefined would be selected as the approach, because it's too likely that someone might accidentally not return anything from their component (which also returns undefined). Goodness knows I've made that mistake myself.

ioss commented

At the moment, I am in favor of only rendering, when it is needed, opposed to bailing out while rendering. We'll always have the possibility to return the previously memoized child components.

To achieve less rendering, I'd say we need / should have:

  • a way to let useState and even more important useReducer not cause a rerender, if the state does not change (referential equality would be sufficient).
  • a way to let useContext not cause a rerender, if we are not interested in the specific context change. I'd add a function to useContext as the second-parameter, either as proposed by @sam-rad here #14110 (comment) or as a predicate (well maybe with two parameters :) ): (previousContext, currentContext) => boolean
  • I would also allow for a predicate at every place, where an array of values can be used useEffect/useCallback/...
    (we could also pass in previousand next values into these functions to make life easier.)
  • And maybe we should have a React.forceUpdate, so we do not have to abuse useState's setState for that.

While everything can be kind of solved with useRef, setState as forceUpdate, etc. or returning memoized children, I'd rather be able to use the base hooks without causing unwanted rerenders and don't have to use multiple hooks just to implement non-rerendering versions of the plain react hooks.

@ruifortes: Personally I wouldn't be to happy to give undefined as return value such an important meaning. Also returning something from render to mark "bail out", wouldn't allow customHooks to "bail out". As explained above I'd rather be able to write simple customHooks, which will only cause needed rerenders.

I agree that having this single "omnipotent" (and I don't mean idempotent) function called all the time is a little weird but I think it's inline with the new hooks paradigm that actually is a lot about memoization.
I like the fact that React avoids "magic" and tries to be as pure and barebone as possible and although this "managed sequential memoization" seams a little magical it really isn't. (and correct me if I'm misusing definitions here cause my background is not in CS)

At the moment, I am in favor of only rendering, when it is needed, opposed to bailing out while rendering. We'll always have the possibility to return the previously memoized child components.

The don't think that you're "rerendering" just because you called this omni function if it doesn't put in motion any expensive processes like DOM diffing.

I would argue that some bailing mechanism is necessary because this "omni" function is not just about rendering but mainly about state because you need to run it to read state.

Preventing this function from being called seams more confusing to me.

You're arguing that "useContext" should accept a selector that prevents calling this omni function again as well as "useState" should do diffing.
That could be a "nice helper" but not so pure and barebones.

With the bailing mechanism you could even take out "useReducer" and "React.memo" and even make "useContext" more like current setState (or is observedBits such on important optimization?)

"useEffect" and "useLayoutEffect" can't go anywhere because they mark important key positions in the actual rendering process.

Basically you can do all props, state and context diffing in this omni function as long as you call hook in the same sequence before bailing.

I don't get what wrong with exploring the semantics of "undefined" and "null" in the return value.
Having useCallback function called in the wrong scope is way more annoying. Wouldn't this also create memory leaks?

Using some "useShallowDiff" hook (that's just an helper hook and could have a better name. useChanged?? isChanged?? "use" is weird here). It's better to return true for changes though.

Something like this (using some counters just as POC testing and didn't actually run it):

function(props) {

 const {someInlineCallback, hide, ...rest} = props

 const [callCounter, setCallCounter] = useState(0)
 const [RenderCounter, setRenderCounter] = useState(0)

 // setCallCounter NOT called if changed prevents infinite loop. Does this really work??? not sure
 // hook can be used in the condition as long it's always called...right?
 if(!useShallowDiff([callCounter])) setCallCounter] (counter => counter +1)

 // you can "render" (because you're changing DOM) nothing returning null
 // Semantically you can read it as actually requiring something to happen as opposed to "undefined"
 if(hide) return null

 // I'm using the bitwise assignment because it always calls the hooks and it's quite practical here.
 // That why I prefer the hook to return true for changes...you can't go the other way around
 var shouldUpdate |= useShallowDiff(rest)

 // would there be any problem if the following hooks are not always called if order is kept?
 const {c1, c2, ...dontneedthesebutcanhavethem} = useContext(appContext)

 // the hook shallow compares all arguments so you need to wrap them in array here
 shouldUpdate |= useShallowDiff([c1, c2])

 const {device} = useMediaQuery()

 shouldUpdate |= useShallowDiff([device])

 // of course you could also call it only once like this:
 // shouldUpdate |= useShallowDiff(rest, [c1, c2, device])
 // or even
 // if(useShallowDiff(rest, [c1, c2, device])) return


 if(!shouldUpdate) return

 // the following with trigger another call but would just set state not rerender. Has long as you don't include it it the should update diff of course...
 setRenderCounter(counter => counter +1)

 // you could also skip some expensive child component creating where

 return (
  <div>This is where rerendering would really happen!!</div>
 )

}

I'm I missing something? I normally do :-P

PS: @markerikson: I changed my profile picture :-) ...SimpsonizeMe was free then, hope I'm not infringing :-P

actually the "Cannot update during an existing state transition" is still there...
Think this kind of recursive function could be quite pure couldn't it?
Is there a reason "setState" (not useState, the setter) can't be called directly, outside "useEffect" or "useLayoutEffect"?
There's no way to call a setter before the it's hook anyway and by immediately recalling the function there would be no side effects to worry about...right?
Calling setState asynchronously would have to be done in an effect hook though...

I like it @sam-rad 's way, that is, component is marked for rendering according to selector (if provided):

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.

It would be convenient to implement useSelector with this approach

useSelector(/* [ */ context /* ] */, props, {
  someValue: someValueSelector,
  ...
});

There's something troublesome about the premise here.

If you're recomputing a render function, you're better off doing the comparison at the end because at the end you have the most information about whether that computation will yield the same effective value or not. I.e. this is equivalent to the returned child being wrapper in a memo HoC for the type or useMemo around the element.

Also, there is some cost to recomputing the render function. If you have lots and lots of things listening to your context value this can add up. However, that's not the extent of the cost, we also have to traverse down the tree and clone all parents to get to a point where we can bail out. It also adds some small cost to all renders.

I think that for most cases, the granularity of the bailout we're talking about won't matter. However, for the cases where it does matter, you probably want it to be even faster than what these optimizations above would get you.

The fastest would be if we don't even have to traverse down the tree nor call the render function. That's what you get for free by comparing old state and not calling setState at all. That should be the goal.

useState

So here's the plan for useState:

  • useReducer will change semantics so that the reducer function used is the one defined by the previous render, not the next render. We could potentially enforce it to be the same one each time too. useReducer and the useState reducer are now allowed to sometimes be called outside the render phase (Suspense might still be allowed).
  • When an action is dispatched to useReducer or setState is called on useState, we will synchronously check if that is the first update in the queue. If it is, we'll synchronously invoke the reducer. If that throws, we'll add the reducer to the queue. If it returns the same value as the base state, then we will ignore the update and not schedule any work. If it is a new value, we'll schedule that new state in the update queue. No need to call the reducer in render. If there are already other items in the update queue then we'll still add it to the queue and schedule an update that rerenders.

This has the effect of most common updates being ignored. It is possible that you add one update and then adds another update that reverts it to the base state. This approach would unnecessarily schedule renders in that case. However, this is an unusual case and when those two states have different priorities, it's a feature to be able to show intermediate states.

forceUpdate

This also has the effect of making mutation not able to rerender. E.g. this pattern breaks:

let [obj, updateObj] = useState({counter: 0});
function onClick() {
  obj.counter++;
  updateObj(obj);
}

We don't want to encourage mutation of state objects. It is already kind of a mess to use React with mutation this way and concurrent mode just makes it worse.

You can still use them if you're careful though and we already have a way to store mutable objects; useRef. You just need a way to trigger the update.

let obj = useRef({counter: 0});
let forceUpdate = useForceUpdate();
function onClick() {
  obj.current.counter++;
  forceUpdate();
}

Until we know how frequently this pattern is needed in modern code, we don't have to make it built in. It is easily implemented in user space:

function forcedReducer(state) {
  return !state;
}
function useForceUpdate() {
  return useReducer(forcedReducer, false)[1];
}

useContext

Context is tricky because we want to try to avoid storing unnecessary state and causing a lot more hidden work to happen. useContext is pretty light weight to use a lot for reads. If we add custom comparisons and stuff it can suddenly become a lot heavier and there's little to indicate in the API that it is.

However, we do have a pretty lightweight mechanism for this already. The changed bits API. It doesn't require storing the old state for comparison at each node. Instead it uses the provider to store a single copy of the old value which can be used for comparison so it doesn't scale up with number of readers.

We could do a similar thing for custom selectors.

I don't know what is best. Either it takes a selector and only returns the value of the selector:

let abc = useContext(A, a => a.b.c);

You can use multiple useContext calls to read multiple values.

let abc = useContext(A, a => a.b.c);
let ade = useContext(A, a => a.d.e);

Another idea is that the selector isn't actually used for extracting the value. It's only used to extract a dependency array similar to how useMemo accepts an array and you promise only to read from those values.

let a = useContext(A, a => [a.b.c, a.d.e]);
let abc = a.b.c;
let ade = a.d.e;

Which of these APIs doesn't matter. The important part is how the mechanism works.

The target context hook object only stores the selector function. This is also where we store which bits a listener is subscribed to. These are not stateful and don't have to be reconciled against each other (unlike state or memo).

When the Provider updates, we compute the changed bits, scan the subtree for all context listeners for this particular context and if those bits overlap with the listened to bits. If it does we need to mark that subtree to be traversed and cloned, but if it doesn't we don't have to walk down that subtree at all. We can do the same thing with a custom function instead of changed bits.

Whenever Provider updates, we scan the tree for contexts. If those have a selector on them, we call the selector with the old context and the new context. If the resulting value(s) doesn't match, we mark it to be traversed. But if it does match we don't have to do anything and we avoid walking down the tree and rerendering.

The major downside of this approach is that the scanning of the tree is a synchronous operation that scales up with number of nodes. We only accept that because we know we can optimize that path to be extremely fast since it's a single hot loop with no function calls and trivial operations that fit in registers.

If we added this capability we might have to start calling functions in this loop which would make it radically slower. More over since these functions are arbitrary user code, it is easy to put expensive stuff in there accidentally.

That said, I don't think this feature is actually that useful. Perhaps even the changed bits one isn't. If you care enough about the performance of this particular update not overrendering, then you have to be expecting very few rerenders (maybe just one?) based on the overall set of many. You're probably operating at a timescale where a few extra renders matter and at that time scale maybe context scanning matters too. So you're might end up using subscriptions instead of this feature anyway. E.g. setting changed bits to zero and then manually force updating the relevant children.

Luckily, this API can be added after the fact so we can add it after the MVP release of hooks. We can discuss it in a separate RFC along with "changed bits" and a subscription based context API.

If you really need one, this is what I think is the cleanest way to implement a force update.

function useForceUpdate() {
  const [,setState] = useState(Symbol())
  return useCallback(() => { setState(Symbol()) }, [])
}

I am also a "dirty modern JS user" πŸ˜‡

But this also takes up 2 slots in the hook list, while your useReducer only takes 1.

A standalone forceUpdate also has an unfortunate effect of the semantics being tied to a component boundary. Everything else allows us to change the component boundary since it's modular.

E.g. we can always inline one component into its parent and preserve semantics.

We can also outline a component. We can extract a set of hooks into a new component if they're self-contained. useForceUpdate doesn't say which refs belongs with it so if you extract it, it no longer rerenders the right component.

So I think I'd prefer something more like let [value, forceUpdate] = useMutableState(() => ...); as the API if we make a built-in one.

That said, I don't think this feature is actually that useful. Perhaps even the changed bits one isn't. If you care enough about the performance of this particular update not overrendering, then you have to be expecting very few rerenders (maybe just one?) based on the overall set of many.

This is exactly the problem redux is running into. A react-redux provider is (almost always) the top level component in any react-redux application, and to prevent tearing (where the redux state can be updated by dispatches in the middle of a single react reconciliation) it also puts the current state in the provider, thus using React to ensure a full reconciliation + commit pass only ever uses the same view of the state.

Because of this, any update to any part of the redux tree, however small and/or irrelevant, causes every single component with a dependency on the react-redux context to update.

Historically, even before PureComponent has a thing, react-redux has always implemented PureComponent-like behavior on things connected to the state because of how easily this can explode.

In other words, what is desired for react-redux is to be able to add the specific values read from inside the context to the list of things React.memo checks before invoking render on a context update, if the component is inside a React.memo.

This is the simplest and extremely performant way I have found:

  1. use a behaviorSubject as a store.
  2. passing down stores by useContext
  3. subscribe to stores and props, do calculation, set the result to local state if it changes(this triggers update).

createStore from https://github.com/buhichan/rehooker

and

useObservable from https://github.com/LeetCode-OpenSource/rxjs-hooks

const ctx = React.createContext({
  store1: createStore({}),
  store2: createStore({}),
});

function Component(props) {
  const {
    store1,
    store2
  } = useContext(ctx);
  const calculatedData = useObservable((inputs$) =>
    inputs$.pipe(
      mergeMap(([s1, s2, dependentProp]) =>
        combineLatest(s1.stream, s2.stream)
        .pipe(map((v1, v2) => {
          // expensive calculate with v1, v2, dependentProp
          return .......;
        }))
      )), {},
    [store1, store2, props.dependentProp]);

  return ......;
}

mutations:

store1.next(oldState => newState);

Let's say there is a hypothetical hook that interacts with React.memo itself (if React.memo's fiber type is or otherwise owns this fiber):

const foo = useMemoContext(
  ReactReduxContext,
  (props, { state }) => state.foo
)

If the component is not wrapped inside a React.memo the hook would just behave like (0, arguments[1])(props, useContext(arguments[0])).

If it is, it would add, say, a list of conditions (other than memoizedProps) to check to the fiber; probably can't use memoizedState for this.

These conditions would then be checked in addition to the shallow equality React.memo already does to the props. This would require an API break to the propsAreEqual second argument to React.memo, though, to be able to receive the list of conditions to check.

I should be clear that Redux causes unnecessary problems for itself due to the API. The api groups many different stores and types of objects into a single atom. Redux also doesn’t have a way to distinguish some parts of the state as having different expectations. Such as some fields expecting to update at high priority and update very few components and other fields update at low priority which might affect many components. The API doesn’t let the framework tell them apart. Trying to create a general state store that merges all types of state into a single atom that updates at the same priority seems like a problematic API design. I don’t think supporting that particular API is a strong motivator for picking a design for these new APIs.

A better example is if you split out each type of store into a separate context and allow each store to have different update strategies.

Hm... maybe a factory API similar to React.createContext which produces a provider/consumer pair, but which uses redux as the engine for processing updates?

Right now the API is specified having a global ReactReduxContext that is shared unless overridden in specific instantiations of Provider or connect()(). This would also require all other react-redux middleware to be converted to this factory style.

But maybe it could work...? πŸ€”


The tough part is, what if you do need 2 of these contexts to communicate with each other? Not one-way, but bidirectionally? There's not really any way to do this right now with the context API, but it is an important and useful property of the single store design.

Perhaps we need a new state container pattern, but I can't think of what it is.

@Kovensky

The tough part is, what if you do need 2 of these contexts to communicate with each other?

I refactored one of my rich-interaction applications to multiple stores and have found that the multiple store should be as a service, implemented following the least expressiveness principle and controlled in a DDD way.

And when it's difficult to manage the communication between stores, it indicates that your store design don't follow the least expressiveness principle and don't have a clear DDD structure. That means it's time to refactor your store.

The main idea I found is distinguish the event, action and mutation.

I mean
events(as a stream) -> action -> mutations(to multiple stores) -> stores -> newStates -> view -> events

Many people use redux, often have this pattern: one event trigger one action and cause one single mutation.
It couples different type of mutation and lead to unnecessary information connection between different part of state.

I go a little far from context api, but it's important that context api provide a fractal support which makes our DDD structure reusable with context api. That's very important.

The context shouldn't be the store (or service) itself.
A service may have multiple stores and child services and control them together, and the context api should provide services instead of being service or store.

I think we might want to have a marker component or string to tell React nothing has changed in the component tree. In this code below, we return React.NoUpdate when there is no change to our DOM.

let oldState;
function ContainerComponent() {
   const context = useContext(MyContext);
   if (context.someState === oldState) {
       return React.NoUpdate;
  } else {
    oldState = context.someState;
  }

  ....
  rest of render method
}

I.... have a lot of thoughts on this topic.

For the moment, I'll keep it short and simple.

We specifically switched React-Redux from "put the store in legacy context, and have individual components subscribe" to "put the store state in new context, and pass it down from the root", for several reasons. I talked about those in my post Idiomatic Redux: The History and Implementation of React-Redux.

Summarizing:

  • Passing down the entire store state in new context gives us a consistent value across the entire tree, which works better with concurrent React
  • Related to that, no "tearing"
  • We get top-down updates for free, instead of having to write our own custom tiered subscription logic
  • Fixes bugs with mixing old and new context together

connect works great overall in v6. However, to go back to the starting point of this thread: in a hooks-centric world, users won't be wrapping their components in connect - they will want to have a useRedux() hook they can call from within their own function components. Since the entire store state is put into context, and calling useContext() subscribes that component to any update of the context value, right now any end-user component that calls a notional useRedux() hook inside of itself will end up re-rendering on every store update, regardless of whether its derived data actually changed or not. (There's already a lot of community interest in having a useRedux hook available.)

Simply put, we need some way to bail out in that scenario, and React does not currently give us the primitives we need to implement that behavior.

I'll point out that, by my estimates, roughly 50% of React apps use Redux. So, coming up with solutions for this will benefit a very large percentage of the React community.

I'll also point out that React-Redux is just one specific instance of this scenario, although probably the most popular one. I'm sure many other libs and scenarios would benefit from this ability.

I have many many other thoughts around changed bits, context, optimization, performance, update priorities, and whatnot, but I'll save those for later. Very happy to discuss them in more detail.

edit

Okay, one additional thought.

Just because the current React-Redux API doesn't allow use of differing update priorities, doesn't mean it can't allow that in the future.

For example, I can imagine a React-Redux-specific store enhancer that users could add to their store setup, which could watch for metadata fields on actions like {type : "INCREMENT", meta : {priority : "low"}}. We could then make use of that user-defined priority somewhere in the process of <Provider> subscribing, getting the latest state, and calling setState().

I'm still not clear on exactly what the React APIs for specifying update priorities currently look like and what they'll look like in the future, but if the APIs exist, we can find a way to use them.

All I'm saying is that Redux is picking a certain set of tradeoffs and this space is full of tradeoffs. There isn't a one-size-fits-all solution so we shouldn't build that is. We should build in primitives that let you pick the right tradeoffs in user space.

I agree completely with that comment.

Said this already on Twitter, but I'll repeat here.

I think I'm asking for primitives that we can build on :) I'm plenty happy to build Redux-specific solutions in userspace, I just don't think we have all the primitives we need right now. I do think that whatever primitives we come up with that benefit Redux would benefit other libs and use cases as well.

I will happily discuss this in depth in whatever forum you prefer.

To get the effect of global store like Redux, we can subscribe to the store from a hook and when relevant state changes, update the state of the component. I don’t think we need another primitive for this. Redux store has a subscribe function and useRedux hook can use it! Some code like this:

import { useEffect, useState } from 'react';
import { subscribe, unsubscribe, dispatch } from './store';
import shallowEqual from './shallowEqual';

let oldState;

export default function useStore(mapContextToState, initialState) {
    const [state, setState] = useState(initialState);
    oldState = state;

    useEffect(() => {
        subscribe(handleContextChange);
        return () => unsubscribe(handleContextChange);
    }, []);

    const handleContextChange = (context) => {
        if (typeof mapContextToState === 'function') {
            const newState = mapContextToState(context);
            if (!shallowEqual(newState, oldState)) {
                setState(newState);
            }
        } else {
            setState(context);
        }
    }

    return [state, dispatch];
}

useReducer API is not good enough for global state. As it does not take care of interdependent reducers.

Context API is not good enough because it does not have an explicit subscribe mechanism.

Use redux store subscribe feature to achieve some sort of useRedux hook.

I can second that I need the same feature as React-Redux for usage with unstated Le jeu. 13 dΓ©c. 2018 Γ  21:59, Mark Erikson notifications@github.com a Γ©crit :
…
I agree completely with that comment. Said this already on Twitter, but I'll repeat here. I think I'm asking for primitives that we can build on :) I'm plenty happy to build Redux-specific solutions in userspace, I just don't think we have all the primitives we need right now. I do think that whatever primitives we come up with that benefit Redux would benefit other libs and use cases as well. I will happily discuss this in depth in whatever forum you prefer. β€” You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub <#14110 (comment)>, or mute the thread https://github.com/notifications/unsubscribe-auth/AAtvPoL6q979zoHQMd-UBOSS1HzqQ1hFks5u4r_BgaJpZM4YPFAP .

Just want to add that this feature is also needed for a library I'm working on. I think it's a feature lots of people would get uses out of, more than just Redux for sure

@vijayst: No. Direct subscriptions are likely to cause issues in concurrent React - that's one of the main reasons why we switched to passing down the store state via context.

And regardless, we've established that React-Redux is just one example of the larger issue involved here, that there's no way to bail out if a value derived from context hasn't changed.

@markerikson Can you elaborate on direct subscription being problematic for concurrent react? Is it because it will not have any priority taken into account?

@markerikson I went through the article on react-redux-history. Lot of information. Any short answer to: Why is direct subscription be problematic for concurrent react? I think other packages like Mobx rely on subscriptions. So, will there be problems with Mobx when using concurrent react? Thanks.

I think React has complicated the situation a bit with Context API when they forced the use of value prop in Context.Provider.

Couldn't we reach middle ground between Zero ability to select or a full blown complicated selector?
How about allowing multiple props were React create subcontext - internally - for each prop?

Example:

const customersState = [];
const catalogState = [];
const cartState = [];

const ShopContext = createContext();

const App = () => (
  // create three unrelated contexts
  <ShopContext.Provider customers={customersState} catalog={catalogState} cart={cartState}>
    <ShopComponent />
  </ShopContext.Provider>
);

The above would've created three separate subcontexts: customers, catalog and cart.
Then we can consume those separate subcontexts like:

const ShopComponent = () => {
  const {catalog, cart} = useContext(ShopContext);
  // Customers was omitted and thus updates to customers context will not trigger
  // this component to re-render

  return (
    <>
      {catalog.map((product) => <div>{product}</div>)}
      {cart.map((item) => <div>{item}</div>)}
    </>
  );
};

Yes, that doesn't provide the must optimized, highly-grained access to objects, but it also doesn't treat everything as one single giant object.

IMHO, this look clearer too as the ShopComponent shows, in a very understandable and hooky way, that it has subscribed to catalog and cart but not to customers subcontext, without resorting to the weird selector argument

Another bonus is that you don't have to do the following ugly and fake structure:

const App = () => (
  <CustomersContext.Provider value={customersState}>
    <CatalogContext.Provider value={catalogState}>
      <CartContext.Provider value={cartState}>
        <ShopComponent />
      </CartContext.Provider>
    </CatalogContext.Provider>
  </CustomersContext.Provider>
);

In order to get separate contexts.

The idea here is that react will create different context to each prop

Please React team, reconsider the Context API.

@FredyC, regarding you comment

Well, you are partially right about the need to nest multiple providers for a different kind of values. However the const {catalog, cart} = useContext(ShopContext); is already working if you pass the object inside the value prop of a Provider. Please join a discussion in facebook/react#14110

Sure, but you will get a single context for all of them. That is the issue.

@amshtemp I don't see such a huge concern. As I said above (#14110 (comment)) this is no issue for me. With MobX this is handled nicely without any extra hassle. I am not saying it's a proper React way, but definitely solves the issue in userland. It kinda makes me sad that people still see Redux as the only way and they need to suffer like this.

Folks, this thread is getting pretty far off topic. This is not a place to discuss React-Redux implementation details, concurrent React behavior, compare MobX vs Redux, or complain about the design of the context API. The original point is that there is a missing capability in the current hooks API, and it needs to be filled. Let's keep the discussion focused on specific use cases that would benefit from that capability, and what the API might look like.

I like the API approaches @sebmarkbage outlined for useContext.

If we added this capability we might have to start calling functions in this loop which would make it radically slower. More over since these functions are arbitrary user code, it is easy to put expensive stuff in there accidentally.

If the basic selector approach is adopted and the hook provides reasonable defaults such as === or shallowEqual type checks, I suspect the impact of this would be negligible especially vs lots of unnecessary rerenders.

I keep seeing more and more articles in the community encouraging the use of the Context API to store large amounts state, functions etc. If people keep going down this path using the Context API in this fashion, they'll eventually want/need the ability to only watch a piece of their context or the alternative for large applications is making lots of contexts which can get unwieldily. Basically, I can see this as being beneficial for more than just Redux.

One thing for the React team to think carefully about is a large percentage of React apps depend on and use state management solutions like Redux and not having functionality like this will make it very difficult for these applications to adopt hooks. Anecdotally our company has been in the process for over a year converting a legacy AngularJS application to React + Redux and we're all excited about hooks and we're all afraid that if no solution is reached to this, we won't be able to join the fun! Please don't interpret this as I want the React team to make poor API decisions and we're all super appreciative to the awesome work you do!

@sebmarkbage , you outlined a possible forceUpdate hook. What about a variation such as cancelUpdate()? I don't personally like that but would provide a user with more advanced cases to bail on rendering though I like the selector approach best :).

For context, I'm currently working on how to support state outside of React and still properly support concurrent mode. The current RFC for Context.write won't work out and it turns out that this is a very difficult problem for which we don't yet have any solution. So currently it is not possible to properly support external state in React unless you always put it in a provider in the tree. Needless to say, this is a pretty big limitation.

A lot of the constraints in this thread are moot if we don't solve that. So that's something we need to figure out first and then we can get back to the concern about how to propagate finer grained updates. Whatever the solution is to the multiple roots problem might add further constraints on this that we don't even know yet.

The current RFC for Context.write won't work out

@sebmarkbage Unless I've missed something somewhere, could you or someone else explain why?

@kingdaro We'll post something when we have a clearer idea of how to express the problem. It's pretty hard to explain since it's even vague in our heads what the constraints really are. The RFC even mentions not supporting other async implementations than React and won't support multiple roots. Naively you might think that you can just add that on top but that doesn't actually work without breaking other features.

@sebmarkbage : I guess I have two main questions at the moment:

  • Is the "context write" issue something that would be a blocker for finalizing the release of hooks?
  • Is the "hooks bail-out" issue a blocker for releasing hooks, or is it something you would plan on tackling afterwards?

@markerikson The useState bail-out is a blocker for the first release of hooks since it changes the semantics of the current API. I don't consider the useContext bail-out issue a blocker because any solution is going to add a new non-breaking API change. So we can always add that after the first release.

The "context write" issue is similarly orthogonal to the hooks release so it's not a blocker for hooks. It is a blocker for concurrent mode though.

That was really the high level point of my first comment on this issue. Making sure we clarify the minimal thing we need to release hooks stable - the first iteration.

@markerikson I realize that you've been put in a particularly bad spot because of discussions with our team about concurrent mode. I apologize for that. We've leaned on the use of context for propagation which lead you down the route of react-redux v6. I think it's generally the right direction but it might be a bit premature. Most existing solutions won't have trouble migrating to hooks since they'll just keep relying on subscriptions and won't be concurrent mode compatible anyway. Since you're on the bleeding edge, you're stuck in a bad combination of constraints for this first release. Hooks might be missing the bailout of context that you had in classes and you've already switched off subscriptions. :(

@vijayst regarding your useStore snippet. There are at least 2 issues with it:

  1. If you subscribe to store in useEffect, the subscription callbacks will trigger in the wrong order since useEffect gets triggered for children before parents. And reversing subscriber list is not enough, because different subtrees get rendered in and out over time at different parts of the render tree (think of initial app rendering, and then a modal with subcomponents coming in later, how do you subscribe in the right order). This is why redux used to use a custom subscription tree.
  2. If both parents and children subscribe to related parts of the store, the children will get rendered twice. Because each subscriber will call the local setState and so the parent will rerender the child and the local set will also rerender the child. (Now that I think about it, I'm actually not 100% sure this is right.. perhaps React collapses these inside a tick. Edit: it is right, children get rendered twice).

Both of these issues can be worked around.

But I think there's an additional issue with concurrent mode where there's no longer a singular view of the state during a single render. I do wonder if that's a real issue in practise.

This is why using React's Context is so attractive, it addresses all of these issues. But introduces new ones, thus this thread..

@KidkArolis That's why unstable_batchedUpdates exists so you can group multiple updates into a single render. Then the order doesn't matter.

@sebmarkbage : yeah, talking with Andrew+Brian+Dan, there's certainly been an emphasis on the future issues around concurrent mode. I don't think we jumped the gun on this, exactly - the switch over to passing down Redux store state via context did make a lot of natural sense for this release, and in some ways that's how React-Redux was really meant to work originally. That said, I do find it a bit surprising that I don't see other libraries really talking about stuff like "tearing" much. (Maybe I'm not just looking in the right places, or perhaps there's discussions going on in private venues I'm not aware of.)

I will say that I feel like I'm sorta getting mixed messages from the team as a whole. On the one hand, createContext was immediately promoted as "production-ready" the minute it was released, and the team has added things like static contextType to encourage people to migrate away from legacy context. Between that and people wanting to move away from Redux, that leads directly to the idea that users will be putting large quantities of state into context, whether it be coming from a Redux store, other state libs like Unstated, or just hand-managed state. Then there's things like unstable_observedBits, which clearly looks useful, and the additional discussions around "writing to context". So, it seems as if context has been put out there as the general solution to deal with passing down state

But, at the same time, a lot of your comments specifically are coming across as "well, this thing that looks like it's what exactly what you want really isn't meant to be used the way you think it is". Which may be the case, but that's not the message I've been getting :) (As always, I realize that nuance in text-based discussions can be hard to get across, and even the face-to-face chat we had at ReactConf was admittedly a bit hard to interpret.) The notion that sticking with direct subscriptions might hypothetically be a better route for now definitely isn't something that has communicated clearly. (Side note: I know Andrew recently said he's been trying to write down "principles of concurrent mode", and I remember either he or you made comments on Twitter about external stores "needing to maintain multiple in-flight trees", or something to that effect. I'd love to see more details on that.)

As a maintainer, I'm trying to be sensitive to both where the React team is going, and what our users want to do. I probably linked it already in this thread, but there's a ton of interest in a useRedux hook of some kind (and the React team even mentioned that as a possibility in the hooks FAQ page). It would be ironic and a bit disappointing if we weren't able to meaningfully ship one when hooks do come out as a stable API.

Going back to the start of the thread, I suppose one semi-ugly workaround would be to ship useRedux(), but advise users that they'd have to handle memoization themselves:

function ContextUsingComponent() {
    const {a, b, doSomething} = useRedux(mapState, mapDispatch);

    const children = useMemo(() => {
        return <div>{a.someField}</div>
    }, [a, b]);
    return children;
}

So, all that said, I certainly appreciate you taking the time to add some insight in this thread. I'm still very interested in discussing any of these aspects (context, subscriptions, perf optimizations) with the team at any time.

Last question for now, I guess: since a bunch of notional "bail out" APIs have been suggested in this thread, what do you see as the likeliest possibilities for that?

"well, this thing that looks like it's what exactly what you want really isn't meant to be used the way you think it is". Which may be the case, but that's not the message I've been getting

I agree that this has been messaged poorly and I don't think everyone on the React team agrees with me here. I believe a lot of people want it to be something that it can't be because it would be such a nice story if it was but the reality is that it is steeped in tradeoffs.

My personal summary is that new context is ready to be used for low frequency unlikely updates (like locale/theme). It's also good to use it in the same way as old context was used. I.e. for static values and then propagate updates through subscriptions. It's not ready to be used as a replacement for all Flux-like state propagation.

Idea: what if, in the ReactReduxContext, instead of the currentState, a getState() wrapper was put in? This would return the state as it was the first time it was called, and it would only reset after the entire tree commits:

getState = () => {
  if (this.currentState !== undefined) {
    return this.currentState
  }
  this.currentState = this.store.getState()
  this.setState({ updated: true })
}

componentDidUpdate() {
  if (this.state.updated) {
    this.currentState = undefined
    this.setState({ updated: false })
  }
}

This, however, depends on the Provider only ever calling its componentDidUpdate after all children are committed (and it's on the Provider to return memoized children to prevent cascading updates). I don't know if React won't accidentally treat this setState as higher priority than any children updates.


I also have no idea how to deal with the problem of cascading redux store updates, because if any cascading update happens with this very simple idea it'll just get thrown out until the next React update. Clearly something more sophisticated than this is needed but it's a possible thing to try to avoid putting a frequently mutated object on the Context.

It's not ready to be used as a replacement for all Flux-like state propagation.

Oooo-kay. Yeah, that statement would have been nice to hear before we hit the button to publish React-Redux v6 final :)

I mean, it does seem to be working fairly well overall, which is why we went ahead and released v6 (with some caveats about possible max perf compared to v5). As I said in that post:

It's possible that users with truly demanding performance requirements may determine that they need to stick with v5 for the time being, at least until React has made some optimizations to how context updates are handled. Or, others may decide that compatibility with async React is more important, and that they're ready to update to v6.

I'm curious - can you elaborate on what specifically makes new context "not ready to replace Flux-like state propagation" ? Is it primarily the cost of walking the tree to find consumers? (I realize this may be more suited for discussion in #13739 .)

My interpretation was that our in-person conversations and with Andrew were pretty clear about this but I suppose it's hard to tell.

The React-Redux v6 solution is fine if you're ok with those perf characteristics. We don't support fine grained invalidation. E.g. based on selectors, changed bits, changed key maps, subscriptions etc. Mostly because there are many different implementation strategies here and we don't know yet which one will work best. The nice thing about subscriptions is that it works in user space today.

Another clarification: We do think new context is a requirement for supporting concurrent mode properly but it's not sufficient alone to support it. You need more building blocks than only new context to make it work.

Another clarification: We do think new context is a requirement for supporting concurrent mode properly but it's not sufficient alone to support it. You need more building blocks than only new context to make it work.

This is the part I regret the most about our messaging. We wanted to communicate that the context API was designed with concurrent mode in mind but what people inferred was that it's a total solution for concurrent support.

Yeah, I think I went into that conversation wanting to hear certain things, and the discussion spiraled off in directions I wasn't anticipating. (I also generally had way too much stuff running around in my head and not enough sleep, so the finer nuances of the discussion probably didn't sink in very well.)

@acdlite : fwiw, I definitely understand that more stuff is needed for concurrent support, per our whiteboard discussion earlier this year. My goal for React-Redux v6 was that it would at least be a step towards compatibility, not that it would actually be the "solution".

@vijayst @sebmarkbage yes, unstable_batchedUpdates is a good solution for out of order and double rendering issues. I will definitely have a closer look at that.

But just wanted to make sure that the point gets across that in that implementation above, those issues do exist. If you dispatch 3 updates to the store, the app rerenders 3 times and the children rerender 6 times, and the children rerender before parents.

Of course, it depends on a few things: a) the store implementation could counter these (e.g. by utilising unstable_batchedUpdates internally and b) you won't experience those issues if you're updating the store in a user event callback, since React wraps event callbacks in unstable_batchedUpdates or equivalent.

And just for anyone following along.. Here's a demonstration: https://codesandbox.io/s/20w7l6zo3y.

And what about this redux-react-hook from facebook itself? A quick glance at the README makes it look like they've achieved it* in combination with requiring users to useCallback in a couple of places.

* By "it" I mean using context with a partial selection of state, and preventing re-render if that partial state has not changed, even if other parts have changed. It is not a "bailing out" mechanism, but still.

@gnapse : looking at the code, that third-party hook appears to pass the store itself down via context, and subscribes to the store directly. That is equivalent to how React-Redux v5 works. The discussion in this thread is how to allow hooks and behavior that match the way React-Redux v6, Unstated, Formik, and other similar libraries work: passing the entire state object down via context. (Also, note that that's "just" from a Facebook employee, not specifically a Facebook-sponsored library, and that Facebook does not own or work on Redux directly.)

Yes, sorry. I know it’s not an official Facebook solution and my way of putting it made it seem so.

Also, at the risk to continue to take this thread for stuff that’s not specifically about bailing out: is there something wrong with this approach? Maybe it’ll be problematic with concurrent react and suspense? If not, what’s the problem with continuing down that path?

@gnapse : yes, as has been discussed multiple times in this thread already, external mutative stores and subscriptions are likely to not work correctly under concurrent React and Suspense.

Semi-new to react and came across this problem as well. The useContext is re-rendering our whole app on every change.

Any chance to create something similar to the connect HOC from redux?
Something like:

import React, {useContext} from 'react'
import contextName from 'contextLocation'

const Component = ({ contextVariable }) => {
     return (
          <div>{contextVariable.b}</div>
     )
}

export default useContext(contextName, contextVariable, fn)(Component)

Where fn is the should component update function.

This way it might be able to stop the re-render if the function comes back false?
You may also be able to subscribe to multiple contexts this way by turning the first two variables into arrays or adding another HOC.

@ccmartell That's not really a hook, that's really just a HOC, so I wouldn't use use in the name.

With or without hooks you should be able to do that using recompose:

export default fromRenderProps(TheContext.Consumer, ({ contextVariable }) => ({ contextVariable }))(Component);

Then there's with-context and perhaps some other render prop helpers.

const themeContext = React.createContext({color, size, opacity})

function App({msg}) {
  const {color, size} = useContext(themeContext, ({color, size}) => ({color, size}))

  useOptimize((nextProp, nextState, nextContext) => {
    return nextProp.msg === msg && nextContext.color === color && nextContext.size === size
  })

  /* ... */
}

useOptimize(isEqual = shallowEqual)

example:
<App msg={'hello'} />
status of App: {props: {msg: 'hello'}, state: undefined, context: {color, size}}

Whatever the solution is to the multiple roots problem might add further constraints on this that we don't even know yet.

Naively you might think that you can just add that on top but that doesn't actually work without breaking other features.

@sebmarkbage this might be what you've hinted in with being a "naive" solution.. but, just to put it on the table..

we already have a solution for rendering into multiple physical dom roots while still keeping a single virtual root using portal.. so, I wonder if the multiple roots problem can be solved by react automatically render a single shared root whose sole responsibility is to hold the state and render each "mount point" using portal inside a context provider..

@ignatiusreza That works for some cases but it only works with one React renderer/version at a time and don't play nicely with non-react code. It also doesn't let you coordinate the updates to individual roots with other content on the page - like the createBatch API in createRoot lets you do.

@sebmarkbage thanks for the insight! if you don't mind, I still have some clarification I want to ask πŸ™‡β€β™‚οΈ

and don't play nicely with non-react code.

sorry, i'm not sure why it doesn't play nicely with non-react code.. do you mind to elaborate a little?

It also doesn't let you coordinate the updates to individual roots with other content on the page

doesn't it means that portal also suffer from the same issue, which we potentially need to fix?

@sebmarkbage Why not try Proxy to optimize performance? seems it can work well.

const {color, size} = useContext(themeContext)

themeContext's value is wraped by Proxy, so we can know color and size are get in this component, we can use shallowEqual to optimize by default.

new Proxy(themeContext.value, {
  get() {/* ... */}
})

If color and size were not changed, React could skip render this component.

@sebmarkbage
Even more, we can use Proxy to do some amazing things.

function magicObject(parentObject, key, object) {
  return new Proxy(object, {
    set(target, prop, newValue) {
      if (newValue !== target[prop]) {
        parentObject[key] = magicObject(parentObject, key, object)
      }
      target[prop] = newValue
      return newValue
    }
  })
}
var a = {b: {c: 1}}
a.b = magicObject(a, 'b', a.b)

var b1 = a.b
a.b.c = 100
var b2 = a.b

console.log('b1 === b2', b1 === b2) // print `false`

It can work with shallowEqual.
And it can optimize performance for large array when shallow equal.

@gaearon @sebmarkbage
Work with Context:

context.value = {array: []}
magicAnObject(context)

then, context.value will be replaced with new Proxy as array was changed such as array.push(1) every time.
It is very friendly for shallowEqual.

Follow this rule, we can always use shallowEqual for optimizing performance without writing custom shouldUpdate.