Home > Software engineering >  Why does Map updated twice with setTimeout and React in Strict Mode
Why does Map updated twice with setTimeout and React in Strict Mode

Time:11-27

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?

duplicated

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.

  • Related