I have following React component:
App.tsx:
function App() {
const [countdownTimers, setCountdownTimers] = React.useState<
Map<number, number>
>(new Map([[1, 60]]));
useEffect(() => {
const timeoutId = setInterval(() => {
setCountdownTimers((prevState) => {
console.log(prevState);
for (const [timerKey, timer] of prevState) {
prevState.set(timerKey, timer - 1);
}
return new Map(prevState);
});
}, 1000);
return () => {
clearInterval(timeoutId);
};
}, []);
return <>{countdownTimers.get(1)}</>;
};
index.tsx
<React.StrictMode>
<App />
</React.StrictMode>
Code above is expected to subtract 1
from all values in Map
every second. But due to StrictMode
it subtracts 2
. Removing <React.StrictMode>
solves issue, but I want to understand why StrictMode
behave this way only with Map
Could you please advise why it's this way?
CodePudding user response:
In strict mode, state updater functions are invoked twice in an attempt to detect possible bugs.
Your code here does have an arguable bug - you're mutating the existing state in the Map here:
setCountdownTimers((prevState) => {
console.log(prevState);
for (const [timerKey, timer] of prevState) {
prevState.set(timerKey, timer - 1);
}
return new Map(prevState);
});
Although you create a new Map when returning, you're still calling prevState.set
- mutating it. This means that the second time the (strict) state updater runs, the Map it sees (in prevState
the second time) has already had its values decremented once.
Instead of mutating the existing Map, create the new Map immediately, and only change that new Map.
function App() {
const [countdownTimers, setCountdownTimers] = React.useState(new Map([[1, 60]]));
React.useEffect(() => {
const timeoutId = setInterval(() => {
setCountdownTimers((prevState) => {
const newMap = new Map(prevState);
console.log(JSON.stringify([...newMap.entries()]));
for (const [timerKey, timer] of prevState) {
newMap.set(timerKey, timer - 1);
}
return newMap;
});
}, 1000);
return () => {
clearInterval(timeoutId);
};
}, []);
return countdownTimers.get(1);
};
ReactDOM.createRoot(document.querySelector('.react')).render(<React.StrictMode><App /></React.StrictMode>);
<script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<div class='react'></div>
CodePudding user response:
This happens because of React strict mode, it does not have anything to do with the Map
data structure. According to the docs:
Strict mode can’t automatically detect side effects for you, but it can help you spot them by making them a little more deterministic. This is done by intentionally double-invoking the following functions:
Class component constructor, render, and shouldComponentUpdate methods
Class component static getDerivedStateFromProps method
Function component bodies
State updater functions (the first argument to setState)
Functions passed to useState, useMemo, or useReducer
Essentially, the callback passed to the setCountdownTimers
setter is invoked twice, hence subtracting 2
instead of 1
. It shouldn't happen in production.