This is a bug I think.
As the title says, React didn't help me to merge the update requests.
const Player = ({ }) => {
const [name, setName] = useState('John Higgins',)
const [age, setAge] = useState(47)
const nextPlayer = () => {
setName('Mark Selby')
setAge(34)
}
console.log('render.')
return <p>
This player's name is {name}, {age} years.
<button type="button" onClick={nextPlayer}>next</button>
</p>
}
After the button clicked, it outputs:
>> render.
>> render.
But in fact, I got three.
>> render.
>> render.
>> render.
It doesn't always be.
const List = ({}) => {
const [items, setItems] = useState<ListItem[]>([])
const [tick, setTick] = useState(0)
const [page, setPage] = useState(1)
// # effect 001
useEffect(() => {
const refresh = tick > 1
load({ refresh, page })
.then(d => {
if (refresh) setItems(d) // replace
else setItems([...items, ...d]) // append
})
}, [tick, page])
// # effect 002
useEffect(() => {
testingScrollToBottom(() => {
setTick(0) // makes effect 001 applied.
setPage(page + 1) // makes effect 001 applied again.
})
return () => {
stopTestingScrollToBottom()
}
}, [page])
return (
<div>
<button
onClick={() => {
setTick(tick + 1)
setPage(1)
}}>
Refresh
</button>
{ items.map(item => <Item item={item}/>) }
</div>
)
}
Function testingScrollToBottom is where the things happened.
Here is what I did to avoid that:

By the way, testingScrollToBottom uses setTimeout.
For clarification, is this correct?
Given I run my component.
When I click the button multiple times.
Then I expect the console log should only happen twice.
But in fact it rendered three times.
Is that correct?
This issue summary looks incorrect. React is "merging" (we call it "batching") the state updates that happen inside of the click handler. You can see this from the fact that clicking the button once only logs "render" once.
The issue that you may be getting confused about is the fact that licking the button a second time also logs "render" one additional time (but no more).
This does seem a bit unexpected, as I would expect our bailout behavior to be consistent between the 2nd and 3rd+ click.
Repro case posted here: https://codesandbox.io/s/mystifying-mahavira-dmzq7
For clarification, is this correct?
Given I run my component.
When I click the button multiple times.
Then I expect the console log should only happen twice.But in fact it rendered three times.
Is that correct?
Yes, that's it. I will re-edit the content including more details in actual case.
incorrect
This issue summary looks incorrect. React is "merging" (we call it "batching") the state updates that happen inside of the click handler. You can see this from the fact that clicking the button once only logs "render" once.
The issue that you may be getting confused about is the fact that licking the button a second time also logs "render" one additional time (but no more).
This does seem a bit unexpected, as I would expect our bailout behavior to be consistent between the 2nd and 3rd+ click.
Repro case posted here: https://codesandbox.io/s/mystifying-mahavira-dmzq7
Hi, please read my actual code. It's true. I've tested it again and again.
@zxh19890103 The behavior of Player doesn't indicate React is not merging updates. What it shows is something related to bailout, i.e. skipping render in some cases.
In fact, React does batch the updates in the case of Player. The code below shows it clearly.
const Player = () => {
const [name, setName] = useState("John Higgins");
const [age, setAge] = useState(47);
const nextPlayer = () => {
setName("Mark Selby");
setAge(34);
};
console.log([name, age].join(', '));
return (
<p>
This player's name is {name}, {age} years.
<button type="button" onClick={nextPlayer}>
next
</button>
</p>
);
};
When you click the button, the console outputs "Mark Selby, 34". If React doesn't batch the updates, there should be two outputs: "Mark Selby, 47" and "Mark Selby, 34".
However, there're few cases where React doesn't do the batch, among which is setTimeout. That's why your real-world code doesn't work as expected. You can still force batching with ReactDOM. unstable_batchedUpdates:
ReactDOM.unstable_batchedUpdates(() => {
setTick(0);
setPage(page + 1);
});
@zxh19890103 The behavior of
Playerdoesn't indicate React is not merging updates. What it shows is something related to bailout, i.e. skipping render in some cases.In fact, React does batch the updates in the case of
Player. The code below shows it clearly.const Player = () => { const [name, setName] = useState("John Higgins"); const [age, setAge] = useState(47); const nextPlayer = () => { setName("Mark Selby"); setAge(34); }; console.log([name, age].join(', ')); return ( <p> This player's name is {name}, {age} years. <button type="button" onClick={nextPlayer}> next </button> </p> ); };When you click the button, the console outputs "Mark Selby, 34". If React doesn't batch the updates, there should be two outputs: "Mark Selby, 47" and "Mark Selby, 34".
However, there're few cases where React doesn't do the batch, among which is
setTimeout. That's why your real-world code doesn't work as expected. You can still force batching withReactDOM. unstable_batchedUpdates:ReactDOM.unstable_batchedUpdates(() => { setTick(0); setPage(page + 1); });
Thanks, @jddxf. I've known the cause. It's setTimeout for the debounce function in lodash is implemented by the setTimeout.
Hi, please read my actual code. It's true. I've tested it again and again.
I did read your code, at least the code you shared :smile: and I provided a runnable repro that showed why I thought you were mistaken.
It's
setTimeoutfor thedebouncefunction inlodashis implemented by thesetTimeout.
This would definitely cause the behavior you mention, and it's a good example of why it's important to share the full code when reporting a problem.
React batches updates inside of event handlers, but if you use setTimeout before updating- it won't batch them.
Hi, please read my actual code. It's true. I've tested it again and again.
I did read your code, at least the code you shared 馃槃 and I provided a runnable repro that showed why I thought you were mistaken.
It's
setTimeoutfor thedebouncefunction inlodashis implemented by thesetTimeout.This would definitely cause the behavior you mention, and it's a good example of why it's important to share the full code when reporting a problem.
React batches updates inside of event handlers, but if you use
setTimeoutbefore updating- it won't batch them.
@bvaughn Thanks very much ~~. I've understood it. But I'm wondering why React do not put it into consideration? I like using third-party libraries, which will cause many strange phenomenons to be probed.
React's event batching does take things like this into consideration, but we wouldn't want React to be in the business of overriding global/native APIs like setTimeout.
FWIW the upcoming concurrent mode will automatically batch nearby state updates, like in the case you shared, so this wouldn't have been an issue either.
Just suffered from this issue, and am glad to see it's already been reported and has been discussed.
Wanted to offer a minimal reproduction for anyone coming here trying to understand the issue, with setTimeout calling setter functions returned by useState hook, and React not merging (batching) those updates:
function App() {
const [first, setFirst] = React.useState();
const [second, setSecond] = React.useState();
const [third, setThird] = React.useState();
console.log("render: first:", first, "second:", second, "third:", third);
return (
<button
onClick={() => {
setTimeout(() => {
setFirst(1);
setSecond(2);
setThird(3);
}, 1000);
}}
>
Click me!
</button>
);
}
/* Console output on click: */
// render: first: 1 second: undefined third: undefined
// render: first: 1 second: 2 third: undefined
// render: first: 1 second: 2 third: 3
Basically, clicking the button will trigger 3 renderings of the component (one for each setState function called from within setTimeout), each rendering with only a partial state update.
CodeSandbox: https://codesandbox.io/s/unexpected-renderings-with-incomplete-state-update-txhy7
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contribution.
Closing this issue after a prolonged period of inactivity. If this issue is still present in the latest release, please create a new issue with up-to-date information. Thank you!