Consider this trivial example where a function is used to update a useState
object:
App.tsx
import { useState } from 'react';
export default function App() {
const [state, setState] = useState({
id: 0,
});
const update = () => {
setState(prev => {
const id = Date.now();
console.log('previous id:', prev.id, 'new id:', id);
return {
id,
};
});
};
return (
<div>
<div>state: {JSON.stringify(state)}</div>
<button onClick={update}>Update</button>
</div>
);
}
Put this in a standard create-react-app typescript template (with React 17.0.2), and run the app in dev mode. Keep clicking the update button and observe the console. Soon enough one would run into a discrepancy between the intended state and the actual state, e.g.
previous id: 1636301326090 new id: 1636301326260
previous id: 1636301326260 new id: 1636301326440
previous id: 1636301326440 new id: *1636301326611*
previous id: *1636301326612* new id: 1636301326804 // What???
previous id: 1636301326804 new id: 1636301326997
In the console log above, the id: 1636301326612
state came out of nowhere, as the state being set was id: 1636301326611
.
Stranger still, I cannot reproduce this when the app is built for production. Also, if I forgo functional update and just pass an object to setState
as in the commented-out code (infeasible in my actual non-toy code due to atomicity requirement), the issue also doesn't seem to be present.
I'm at a complete loss. Am I using setState
wrong somehow?
I can provide a complete example repo if necessary.
Perhaps this is due to the fact that the setState is called twice in strict mode in development.
Setting the state value does not depend on the previous value, but on the time. This is a side effect.
Related links
https://reactjs.org/docs/strict-mode.html#detecting-unexpected-side-effects
https://github.com/facebook/react/issues/12856