setState() mutations and redux mutations are not batched into a single re-render
See original GitHub issueWhat is the current behavior?
See the codesandbox link below for a running example of the following description.
I have a redux store containing a single array of integers, elems
, initialized to [0]
. I have both an Outer
component and an Inner
component which are parameterized by elems
via connect()
. Inner
stores an index into the array in this.state.index
, initialized to 0
. It also renders a button which, when clicked, will insert a new element into the redux and update this.state.index
to be the index of the new elem.
When the button is pressed, the component is re-rendered twice. During the first re-render, this.state.index
has changed, but not this.props.elems
, so when I index the array, I receive an undefined
result. The subsequent re-render works fine, however.
Note that if I remove the Outer
component, and instead directly render Inner
, there is no issue. Something about having both a parent and a child component subscribed to the same redux data is causing this issue.
If the current behavior is a bug, please provide the steps to reproduce and if possible a minimal demo of the problem. Your bug will get fixed much faster if we can run your code and it doesn’t have dependencies other than React. Paste the link to a CodeSandbox (https://codesandbox.io/s/new) or RN Snack (https://snack.expo.io/) example below:
https://codesandbox.io/s/eager-kepler-coo8g
What is the expected behavior?
I would expect render()
to be called only once, with both this.state
and this.props
updated to their most recent values.
Which versions of React, ReactDOM/React Native, Redux, and React Redux are you using? Which browser and OS are affected by this issue? Did this work in previous versions of React Redux?
See the codesandbox link above.
Issue Analytics
- State:
- Created 4 years ago
- Comments:20 (9 by maintainers)
Top GitHub Comments
We are also experiencing the same issue where a set state call and redux dispatch are not batched into one re-render despite being called within an event handler
I have spent some time digging into this issue and I will share my findings here:
The root cause of the issue is making the ConnectFunction a PureComponent by wrapping in
React.memo()
.Based on what I have read in blog, this step of making it pure was taken as part of performance optimization.
But this optimzation is too aggresive, rather improper because ConnectFunction consumes the state inside the store, which is ‘external’ to this component in the sense that it does not reflect in its props.
A workaround of using
{pure:false}
is available, but it is un-intuitive as your component that is being connected could be a genuinePureComponent
but you will still encounter this bug.Expectation when you are
connect
ing your component is that your component will always be rendered with latest state from store, so this bug is breaking the basic promise ofconnect
.In
v5.0.7
, where Connect was a Class component, it was usingshouldComponentUpdate
to bail out of render when there is no change in component’s props. As Connect is implemented as Function component in v7, there is no mechanism available to bail out a waistful render. I guess making it pure was opted as workaround for this needed bailout.But this workaround is over-agressive. In v5.0.7’s class component,
componentWillReceiveProps
, which is called in every render cycle, executesselector.run
, which will re-evaluatemapStateToProps
if either store state or wrapper props have changed. The bailout happens inshouldComponentUpdate
only if merged props have not changed, so it is conditional as opposed to unconditional bailout in v7’s function component wrapped inReact.memo
.However, wrapping the
ConnectFunction
inReact.memo
gives a huge performance boost. Removing theReact.memo()
results in drastic drop in performance, almost to the level of v6. I have verified this withreact-redux-benchmarks
. So it seems there is no clean solution to this problem.The main motiviation behind move from v5 to v6 and onwards, I believe, was to move from Legacy Context API to New Context API. Usage of function component is strictly not necessary to achieve this. So we could go back to Class component for
Connect
while using New Context API and continue to useshouldComponentUpdate
bailout.Of course this will bring back
StrictMode
warnings, but then question is what is more important - StrictMode compliance or bug free behaviour while keeping performance at acceptable level?