Reputation: 9722
I am trying to figure out when useEffect causes a re-render. I am very surprised by the result of the following example:
https://codesandbox.io/embed/romantic-sun-j5i4m
function useCounter(arr = [1, 2, 3]) {
const [counter, setCount] = useState(0);
useEffect(() => {
for (const i of arr) {
setCount(i);
console.log(counter);
}
}, [arr]);
}
function App() {
useCounter();
console.log("render");
return <div className="App" />;
}
The result of this example is as follows:
I don't know why:
setCount
+ one initial render - so 4 times)Upvotes: 41
Views: 82840
Reputation: 1928
This question and all the answers I read were so insightful to even better understand useEffect and useState hooks because they forced me to dig in to have a depth grasp of those.
Though @ApplePearPerson answer is quite articolate I do believe there are some incorrect aspect and I will point them out with few example:
Component is rendered and so the first "render" in console.
UseEffect run always at least one, after the first render, this basically explain the second render and is the tricky part on why are printed first 0 x ( initial value for counter)
The second argument of the useState hook is an async function thus has async bahavior: it wait other code to run, so it wait the for in block to run.
So the for in block runs and so:
i goes from 1 to 3 with finish value of 3
At this point setCount change counter from 0 t0 3
Useffect runs on dependencies change if there is the array as second argument, so in this case even it is not included, it runs on counter that is been changed from setCount, as you can see even from Eslint warning(React Hook useEffect has a missing dependency: 'counter')
The useState change state cause for the hook one a render(this is why useRef is been introduced for change dom element without cause rerender), though isn't always the case for the setState in class(but this is another topic)
Last render is caused as on each render the arr is re-created, as ApplePearPerson "noticed" but is a complete new array as component is been re-rendered but counter is 3 and is not different from last value that i has, that is exactly 3 as well and so useEffect doesn't run again.
So e.g if we change the for of with a for in, meaning we take the key of the array(that are string) we see that the last value of counter is 2 in this case
https://codesandbox.io/s/kind-surf-oq02y?file=/src/App.js
Another test can be done adding a second counter that is set to the previous. In this case we obtain a fourth Render, as the count2 is behind 1 useffect and it's change from 0 to 3 trigger the last render but not the last useEffect run.
To summurize:
There are 3 Render:
First is due to Component first mount.
Second is due to useEffect run after first Render.
Third is due to change in the dependency from 0 to 3
https://codesandbox.io/s/kind-surf-oq02y?file=/src/App.js:362-383
Upvotes: 0
Reputation: 997
The above solutions very much explained what's happening in the code. If someone is looking for how to avoid re-renders while using default argument in the custom hooks. This is a possible solution.
import React, { useEffect, useState } from "react";
import ReactDOM from "react-dom";
import "./styles.css";
const defaultVal = [1, 2, 3];
function useCounter(arr = defaultVal) {
const [counter, setCount] = useState(0);
useEffect(() => {
console.log(counter);
setCount(arr);
}, [counter, arr]);
return counter;
}
function App() {
const counter = useCounter();
console.log("render");
return (
<div className="App">
<div>{counter}</div>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
Explanation: Since there is no value provided to the custom hook, it is taking the default value which is a constant defaultVal
. Which means arr
reference is always the same. Since the reference didn't change it's not triggering the useEffect hook
Upvotes: 0
Reputation: 2294
There is a coincidence that might create some confusion in the original issue. Mainly the fact that there are 3 renders and the useCounter
has a default param of length equal to 3. Bellow you can see that even for a larger array there will be only 3 renders.
function useCounter(arr = [1, 2, 3, 4 , 5 , 6]) {
const [counter, setCount] = React.useState(0);
React.useEffect(() => {
for (const i of arr) {
setCount(i);
console.log(counter);
}
}, [arr]);
}
function App() {
useCounter();
console.log("render");
return <div className = "App" / > ;
}
ReactDOM.render( <App /> ,
document.getElementById("root")
);
<script crossorigin src="https://unpkg.com/react@16/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js"></script>
<div id="root"></div>
Another confusion might be created by the fact that the setState
is called every time, except the first one, with the same value (the last value of the array), which practically cancel the render. If however the setState
would be called with different values, the presented flow would create an infinite loop :)
because every other render
triggers a useEffect
which triggers a setSate
which triggers a render
which triggers a useEffect
and so on.
Hopefully this makes things more clear for someone.
Upvotes: 0
Reputation: 9722
I found an explanation for the third render in the react docs. I think this clarifies why react does the third render without applying the effect:
If you update a State Hook to the same value as the current state, React will bail out without rendering the children or firing effects. (React uses the Object.is comparison algorithm.)
Note that React may still need to render that specific component again before bailing out. That shouldn’t be a concern because React won’t unnecessarily go “deeper” into the tree. If you’re doing expensive calculations while rendering, you can optimize them with useMemo.
It seems that useState and useReducer share this bail out logic.
Upvotes: 11
Reputation: 4439
I'm going to do my best to explain(or walk through) what is happening. I'm also making two assumptions, in point 7 and point 10.
useEffect
is called after the mounting.useEffect
will 'save' the initial state and thus counter
will be 0 whenever refered to inside it.setCount
is called to update the count and the console log logs the counter which according to the 'stored' version is 0. So the number 0 is logged 3 times in the console. Because the state has changed (0 -> 1, 1 -> 2, 2 -> 3) React sets like a flag or something to tell itself to remember to re-render.useEffect
and instead waits till the useEffect
is done to re-render.useEffect
is done, React remembers that the state of counter
has changed during its execution, thus it will re-render the App.useCounter
is called again. Note here that no parameters are passed to the useCounter
custom hook.
Asumption: I did not know this myself either, but I think the default parameter seems to be created again, or atleast in a way that makes React think that it is new. And thus because the arr
is seen as new, the useEffect
hook will run again. This is the only reason I can explain the useEffect
running a second time.useEffect
, the counter
will have the value of 3. The console log will thus log the number 3 three times as expected.useEffect
has run a second time React has found that the counter changed during execution (3 -> 1, 1 -> 2, 2 -> 3) and thus the App will re-render causing the third 'render' log.useCounter
hook did not change between this render and the previous from the point of view of the App, it does not execute code inside it and thus the useEffect
is not called a third time. So the first render of the app it will always run the hook code. The second one the App saw that the internal state of the hook changed its counter
from 0 to 3 and thus decides to re-run it, and the third time the App sees the internal state was 3 and is still 3 so it decides not to re-run it. That's the best reason I can come up with for the hook to not run again. You can put a log inside the hook itself to see that it does not infact run a third time.This is what I see happening, I hope this made it a little bit clearer.
Upvotes: 43
Reputation: 4987
setState and similar hooks do not immediately rerender your component. They may batch or defer the update until later. So you get only one rerender after the latest setCount
with counter === 3
.
You get initial render with counter === 0
and two additional rerenders with counter === 3
. I am not sure why it doesn't go to an infinite loop. arr = [1, 2, 3]
should create a new array on every call and trigger useEffect
:
counter
to 0
useEffect
logs 0
three times, sets counter
to 3
and triggers a rerendercounter === 3
useEffect
logs 3
three times, sets counter
to 3
and ???React should either stop here or go to an infinite loop from step 3.
Upvotes: 2