Reputation: 2812
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
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>
);
}
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
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