I can’t figure out why React is re-rendering my component but not re-running the useEffect
hooks.
I was playing around with React hooks using this code https://codesandbox.io/s/solitary-wave-s4wt3r?file=/src/index.js
import React, { useState, useEffect, useRef } from "react";
import ReactDOM from "react-dom";
function Example() {
const [test, setTest] = useState("init");
const renderNum = useRef(0);
console.log("renderNum: ", renderNum.current);
renderNum.current++;
useEffect(() => {
console.log("first useEffect value: ", test);
setTest("newval");
});
useEffect(() => {
console.log("useEffect value: ", test);
});
return <div>{test + renderNum.current}</div>;
}
const rootElement = document.getElementById("root");
ReactDOM.render(<Example />, rootElement);
I was surprised by the output:
renderNum: 0
first useEffect value: init
useEffect value: init
renderNum: 1
first useEffect value: newval
useEffect value: newval
renderNum: 2
I would have expected either the last render to never happen (since test
is being set to the same value it currently is) or both useEffect
calls to run.
Any idea why React does the extra render but doesn’t run the effects?
2
Answers
The behavior you are observing is because you didn’t put any dependency in the useEffect, so useEffect will trigger on each rerender on this component, now the component rerender because the state has changed due to setTest("newval")
But this will retrigger once more useEffect hook (since no dependency) and setTest("newval") in the useEffect hook run again … this should have normally triggered an infinite loop even with the same value in setState, however in this case React is smart enough to optimize rendering by using a process called "reconciliation," where it compares the previous render with the current render and decides whether to apply the changes or not, in other word, React performs an optimization that prevents unnecessary rerender when the state doesn’t actually change, so it skip the rerendering and stop at the third rerender.
Anyway this is not really a good practice, you can put an empty dependency to make useEffect render ONLY once, or technically you can also put test state in the dependency but this is a bad example, in theory this can also cause infinite loop since this time useEffect will also run if the test state changed, and you put the setter setTest inside the useEffect containing the test state dependency… it’s just in this case you only use setTest with the same value so the component didn’t rerender.
It’s generally recommended to use useEffect with an empty dependency array [] when you want to run the effect only once on mount, or with specific dependencies that directly relate to the effect’s logic.
In React useEffect, useMemo, and useCallback need you to specify dependencies to know when it needs to be rerun.