2

I have a more complex version of the following pseudo-code. It's a React component that, in the render method, tries to get a piece of data it needs to render from a client-side read-through cache layer. If the data is present, it uses it. Otherwise, the caching layer fetches it over an API call and updates the Redux state by firing several actions (which theoretically eventually cause the component to rerender with the new data).

The problem is that for some reason it seems like after dispatching action 1, control flow moves to the top of the render function again (starting a new execution) and only way later continues to dispatch action 2. Then I again go to the top of the render, and after a while I get action 3 dispatched.

I want all the actions to fire before redux handles the rerender of the component. I would have thought dispatching an action updated the store but only forced components to update after the equivalent of a setTimeout (so at the end of the event loop), no? Is it instead the case that when you dispatch an action the component is updated synchronously immediately, before the rest of the function where the dispatch happens is executed?

class MyComponent {
  render() {
    const someDataINeed = CachingProvider.get(someId);

    return (
      <div>{someDataINeed == null ? "Loading" : someDataINeed }</div>
    );
  }
}

class CachingProvider {
  get(id) {
    if(reduxStoreFieldHasId(id)) {
      return storeField[id];
    }
    
    store.dispatch(setLoadingStateForId(id));
    
    Api.fetch().then(() => {
      store.dispatch(action1);
      store.dispatch(action2);
      store.dispatch(action3);
    });

    return null;
  }
}
Michael Tontchev
  • 909
  • 8
  • 23

2 Answers2

1

You should never invoke heavy operations inside of a render function, since it's going to be triggered way more than you would like to, slowing down your app.

You could for example try to use the useEffect hook, so that your function will be executed only when your id changes.

Example code:

function MyComponent {
  useEffect(() => {
    // call your method and get the result in your state
  }, [someId]);

  return (
    <div>{someDataINeed == null ? "Loading" : someDataINeed }</div>
  );

}
TrinTragula
  • 360
  • 2
  • 13
  • 1
    Thanks, though it doesn't answer my question :) Anyway the results are cached, so it's a fast lookup in a map most of the time. The times that it isn't is the same as if I was using the approach you recommend (which doesn't apply directly to the class case anyway) – Michael Tontchev Feb 09 '21 at 17:38
1

In addition to @TrinTragula's very important answer:

This is React behaviour. Things that trigger rerenders that are invoked synchronously from an effect/lifecycle or event handler are batched, but stuff that is invoked asnychronously (see the .then in your code) will trigger a full rerender without any batching on each of those actions.

The same behaviour would apply if you would call this.setState three times in a row.

You can optimize that part by adding batch which is exported from react-redux:


    Api.fetch().then(() => {
      batch(() => {
        store.dispatch(action1);
        store.dispatch(action2);
        store.dispatch(action3);
      })
    });
phry
  • 35,762
  • 5
  • 67
  • 81
  • Holy crudd that is *so weird*! How does React (or is it Redux here?) know that the caller is inside of an asynchronous context vs a lifecycle or event handler? Also, I tried finding documentation about this but couldn't - do you know where I could read more about this? – Michael Tontchev Feb 09 '21 at 17:45
  • And btw the batch worked like a charm! Thanks for the super fast response! – Michael Tontchev Feb 09 '21 at 17:46
  • Assume you have code like `startTracking(); executeHandler(); endTracking()`. If the code is synchronous, all of it will be executed before `endTracking` is called. If is is asynchronous, `endTracking` will be called before the first `.then` inside the async part, as that only will occur on the next tick. – phry Feb 09 '21 at 17:51
  • So is it not an intentional part of the library? I'm having trouble mapping your answer to my question (in the comments) – Michael Tontchev Feb 09 '21 at 17:54
  • Oh, I think I might understand now. So React internally has something like the following before executing someLifeCycleMethod: startBatching = true, and at the end it flushes the batched setStates. However, when the setState happens outside of a handler, it's not wrapped in the above (and it has no idea whether any other updates might happen or not, so it doesn't know "how long" to batch updates for). Is that it? – Michael Tontchev Feb 09 '21 at 17:59
  • Found some docs on this: https://vasanthk.gitbooks.io/react-bits/content/patterns/19.async-nature-of-setState.html – Michael Tontchev Feb 09 '21 at 18:05
  • And relevant Abramov comment: https://stackoverflow.com/questions/48563650/does-react-keep-the-order-for-state-updates/48610973#48610973 – Michael Tontchev Feb 09 '21 at 18:05