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:
v22.3.0
5.4.1
18.3.1
1.93.1
127.0.6533.120 arm64
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