Simon Lenz
Simon Lenz

Reputation: 2812

React-Redux does not re-render component with two back-to-back actions

I have a component which listens to a certain value in the Redux store with a useSelector. It should re-render whenever that value changes. Though, when I'm dispatching two actions immediately after each other, it skips the first value change and does not re-render the component.

I can fix the issue by wrapping both dispatch calls inside a single setTimout(…, 0). But this is probably a dirty workaround, and I would like to know what is responsible for this mismatch with my expectations in the first place.

A minimal example can be found in this CodePen: https://codepen.io/lenzls/pen/GRMYJMz?editors=1011

Excerpts

function StatusDisplay () {
  const comparisonFunc = (newSelectedState, latestSelectedState) => {
    const equality = newSelectedState === latestSelectedState
    console.log(`[StatusDisplay] — Call comparisonFunc — new: ${newSelectedState} | old: ${latestSelectedState} | equal?: ${equality}`)
    return equality
  }

  const status = useSelector(state => state.status, comparisonFunc)

  console.log(`[StatusDisplay] — !!render component — status: ${status}`)
  
  return <div>UpdatTestComp {status}</div>
}
function App () {
  const changeState = () => {
    console.warn(`PRESSING BUTTON`)
    console.log(`Dispatch status = empty — Expecting [StatusDisplay] to rerender…`)
    store.dispatch({type: 'CHANGE', payload: 'empty'});
    console.log(`Dispatch status = error — Expecting [StatusDisplay] to rerender…`)
    store.dispatch({type: 'CHANGE', payload: 'error'});
  }
  
  const changeStateAfterTimeout = () => setTimeout(changeState, 0)

  return (
    <div>
      <button onClick={changeState}>dispatch two actions</button>
      <button onClick={changeStateAfterTimeout}>dispatch two actions after timeout</button>
      <StatusDisplay />
    </div>
  );
}

Example

On page load, the initial status is error. Console prints

"Initial status: error"
"[StatusDisplay] — !!render component — status: error"

Then I press the dispatch two actions button. Console prints

"PRESSING BUTTON"
"Dispatch status = empty — Expecting [StatusDisplay] to rerender…"
"[StatusDisplay] — Call comparisonFunc — new: empty | old: error | equal?: false"
"Dispatch status = error — Expecting [StatusDisplay] to rerender…"
"[StatusDisplay] — Call comparisonFunc — new: error | old: empty | equal?: false"
"[StatusDisplay] — Call comparisonFunc — new: error | old: error | equal?: true"
"[StatusDisplay] — !!render component — status: error"

We see that the comparison function of the useSelector is actually called immediately after the state change (and before the second action), but the component still does not re-render.

If I press the dispatch two actions after timeout button, console prints

"PRESSING BUTTON"
"Dispatch status = empty — Expecting [StatusDisplay] to rerender…"
"[StatusDisplay] — Call comparisonFunc — new: empty | old: error | equal?: false"
"[StatusDisplay] — Call comparisonFunc — new: empty | old: empty | equal?: true"
"[StatusDisplay] — !!render component — status: empty"
"Dispatch status = error — Expecting [StatusDisplay] to rerender…"
"[StatusDisplay] — Call comparisonFunc — new: error | old: empty | equal?: false"
"[StatusDisplay] — Call comparisonFunc — new: error | old: error | equal?: true"
"[StatusDisplay] — !!render component — status: error"

In this case, the component actually renders twice, once for each change of status. This is what I would expect to happen in either case.


I assume that Redux' dispatch is actually synchronous as described here, but there is some optimization in react-redux going on. I can't find information about it in the docs though.

In general, my actual example is a little more indirect than above, and the two actions are not literally changing the same thing back to back.

Upvotes: 0

Views: 552

Answers (1)

markerikson
markerikson

Reputation: 67499

This is expected behavior. React and React-Redux will batch those dispatches together because they're all occurring in the same event handler and the same event loop tick. So, that will intentionally only result in a single re-render, using the final state.

If you change the handler to run after a timeout, React 17 does not automatically batch those updates together. However, React 18 will batch them. You can get similar behavior with React 17 by wrapping both dispatches in the batch() API exported from React-Redux

Upvotes: 2

Related Questions