Łukasz Jagodziński
Łukasz Jagodziński

Reputation: 3079

Component renders twice on async React context update

I have React application which manages state using React context. I've created simple counter incrementation reproduction.

There are two contexts. One for storing state, and the second one for dispatching. It's a pattern taken from this article.

The state context just stores single number and there is only one action that can be invoked on this state:

function reducer(state: State, action: Action): State {
  switch (action.type) {
    case "inc": {
      return state + 1;
    }
  }
}

I also have two helper functions for incrementing value synchronously and asynchronously:

async function asyncInc(dispatch: React.Dispatch<Action>) {
  await delay(1000);
  dispatch({ type: "inc" });
  dispatch({ type: "inc" });
}

function syncInc(dispatch: React.Dispatch<Action>) {
  dispatch({ type: "inc" });
  dispatch({ type: "inc" });
}

And here is how you use it in the component:

const counter = useCounterState();
const dispatch = useCounterDispatch();

return (
  <React.Fragment>
    <button onClick={() => asyncInc(dispatch)}>async increment</button>
    <button onClick={() => syncInc(dispatch)}>sync increment</button>
    <div>{counter}</div>
  </React.Fragment>
);

Now, when I click the sync increment button everything will work as expected. It will invoke the inc operation twice, incrementing counter by 2 and perform only one rerender of the component.

When I click the async increment button it will first wait for one second and perform inc operation twice but it will rerender component twice.

You have to open console to see logs from rendering components.

I kinda understand why is that. When it's a synchronous operation, everything happens during component rendering, so it will first manipulate state and render at the end. When it's an asynchronous operation, it will first render component and after one second it will update state once triggering rerender and it will update for the second time triggering next rerender.

So is it possible to perform asynchronous state update doing only one rerender? In the same reproduction, there is similar example but using React.useState that is also having the same problem.

Can, we somehow batch updates? Or I have to create another action that would perform several operations on the state at once?

I've also created this reproduction that solves this problem by taking array of actions but I'm curious if it can be avoided.

Upvotes: 3

Views: 1591

Answers (2)

Joseph D.
Joseph D.

Reputation: 12174

Basically what you're seeing for syncInc() is batching for click events. Thus, you only see it render once.

React batches all setStates done during a React event handler, and applies them just before exiting its own browser event handler.

For your asyncInc(), it is outside the scope of the event handler (due to async) so it is expected you get two re-renders (i.e doesn't batch state updates).

Can, we somehow batch updates?

Yes React can batch updates within an async function.

Upvotes: 2

skovy
skovy

Reputation: 5650

Unless this is causing a performance problem I would recommend not worrying about additional renders. I found this post about fixing slow renders before worrying about re-renders to be helpful on this topic.

However, it does look like React has an unstable (so it shouldn't be used) API for batching updates ReactDOM.unstable_batchedUpdates. However, using this could cause a headache in the future if it's removed or changed. But to answer your question "can we somehow batch updates?", yes.

const asyncInc = async () => {
  await delay(1000);

  ReactDOM.unstable_batchedUpdates(() => {
    setCounter(counter + 1);
    setCounter(counter + 1);
  });
};

Upvotes: 1

Related Questions