javascriptreactjsreact-hooks

Nested set function in useEffect called three times in strict mode


I got a strange behaviour in my react code:

Edit: It turns out the clear function was working and successfully cleared the interval. Now I faced another issue from strict mode double invocation. I have attached the full app code below, the set function setRow() inside another set function setSecondPassed() has been called 3 times per interval, and one of the time the value has updated another time. I assume under double invocation and if my code is well written, they should be idempotent but inside each interval the state of rows updated twice.

import { useState, useEffect, useMemo, useRef } from "react";

function App() {
  return (
    <>
      <Timer />
    </>
  );
}
function Timer() {
  const [secondsPassed, setSecondPassed] = useState(0);
  const [rows, setRow] = useState(0);
  const thresold = 10;

  function onInterval() {
    console.log("onInterval timer update"); // once per interval
    setSecondPassed((prev) => {
      const next = prev + 1;
      console.log(`next: ${next}`); // twice per interval
      setRow((prevRows) => {
        const nextRows = prevRows + 1;
        console.log(`setRow: ${nextRows}`); // 3 times per interval
        return nextRows;
      });
      return next;
    });
  }
  useEffect(() => {
    const intervalId = setInterval(onInterval, 1000);
    console.log(`timer setinterval: ${intervalId}`);

    return () => {
      console.log(`timer clearInterval: ${intervalId}`);
      clearInterval(intervalId);
    };
  }, []);

  const fixedRows = useMemo(() => {
    console.log(`fixedRows: ${rows}`);
    return Array.from({ length: rows }).map((_, i) => (
      <div key={i} className="box-x" style={{ width: `${thresold}px` }}></div>
    ));
  }, [rows]);

  return (
    <>
      {fixedRows}
      <div className="box-x" style={{ width: secondsPassed % thresold }}></div>
    </>
  );
}
export default App;

Even I put the code in codesandbox.io. The log still showing the set function setRow() has been called 3 times per interval. From the React documentation, there should be double invocation in strict mode. I am not sure why it has been called 3 times. Also I would like to have my code being idempotent as much as possible so the result from strict mode double invocation will not affect rendering result. Any suggestion is appreciated.

fixedRows: 0
preview-protocol.js:56 fixedRows: 0
App.jsx:30 timer setinterval: 17
App.jsx:33 timer clearInterval: 17
App.jsx:30 timer setinterval: 20
index.js:8 onInterval timer update
index.js:8 next: 1
index.js:8 setRow: 1
index.js:8 fixedRows: 1
index.js:8 fixedRows: 1
index.js:8 onInterval timer update
index.js:8 next: 2
index.js:8 setRow: 2
index.js:8 fixedRows: 2
index.js:8 next: 2
index.js:8 setRow: 2
index.js:8 setRow: 3 <- extra state update here
index.js:8 fixedRows: 3
index.js:8 onInterval timer update
index.js:8 next: 3
index.js:8 setRow: 4
index.js:8 fixedRows: 4
index.js:8 next: 3
index.js:8 setRow: 4
index.js:8 setRow: 5 <- extra state update here
index.js:8 fixedRows: 5

Environment:


Solution

  • By extracting the nested set function out with the use of another useEffect(), I've managed to make the state change of rows in a more idempotent manner under double invocation in strict mode, the log now shows 2 entries per interval instead of 3, as expected.

      useEffect(() => {
        if (secondsPassed % thresold === 0)
        {
          setRow(prevRows => {
            const nextRows = prevRows + 1;
            console.log(`setRow: ${nextRows}`);
            return nextRows;
          });
        }
      }, [secondsPassed]);
    
    App.jsx:42 timer update
    App.jsx:45 next: 10
    App.jsx:45 next: 10
    App.jsx:54 setRow: 3
    App.jsx:54 setRow: 3
    

    The lesson I've learned is not to nest set function as the state change will not easy to determine. Thanks the help from @WeDoTheBest4You and @Scott Z