useEffect Hook Example: What causes the re-render? - reactjs

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:
The component renders only three times (I would have guessed the component would rerender for every call to setCount + one initial render - so 4 times)
The counter only ever has two values 0 and 3: I guess, as this article states, every render sees its own state and props so the entire loop will be run with each state as a constant (1, 2, 3) --> But why is the state never 2?

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.
App component mounts.
useEffect is called after the mounting.
useEffect will 'save' the initial state and thus counter will be 0 whenever refered to inside it.
The loop runs 3 times. Each iteration 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.
React has not re-rendered anything during the execution of useEffect and instead waits till the useEffect is done to re-render.
Once the useEffect is done, React remembers that the state of counter has changed during its execution, thus it will re-render the App.
The app re-renders and the 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.
During the second run of useEffect, the counter will have the value of 3. The console log will thus log the number 3 three times as expected.
After the 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.
Asumption: because the internal state of the 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.

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.

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:
initial render sets counter to 0
useEffect logs 0 three times, sets counter to 3 and triggers a rerender
first rerender with counter === 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.

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.

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

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.
This screenshot can help to visualize my summary
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

Related

Looking for difference between useEffect and code written in the function

My understanding was that useEffect ran whenever the dependencies in the array were rerendered (or only at the first render in if it's blank) or whenever ANYTHING rerendered if there was no Array.
I also thought the code directly in the function about return (not inside a hook though) i.e. like how you declare variables to hold values of setState, or declared functions, only ran "once" (though i'm not sure when)
However, in the example below, I saw the console.log statement run multuple times, almost in tandem with useEffect. Specifically it seemed that the console.log was running in some weird offsync pattern with useEffect, but I don't understand why it would be running, if my variables aren't being redeclared and such. Like, even if it's on every render, similar to a useEffect with no specified dependencies, wouldn't it then also be reintializing the useState variables and such??
So my questions are
When does the code in a "raw" functional component get run, i.e. the console.log("FLOATING CODE") -> I would either think it only ran on initialization OR it ran every rerender, but neither of these seem to be the case. The question is based on the discrepancy between how the functional code reruns, but the initialization code doesnt.
Also why does it run TWICE
why is the value of "log" different on the web page and in the console.log? The setLog is literally before the console.log(), shouldn't it update FIRST? Esp given that the update clearly goes through for the page text to rerender.
import React, { useState, useEffect } from 'react';
const App = () => {
const [num, setNum] = useState(1);
const [log, setLog] = useState('');
const add = () => {
setNum(num + 1);
};
useEffect(() => {
setLog('useEffect has run-> NUM was rerendered ' + num);
console.log(log)
}, [num]);
// setLog(log + " floating code has run");
console.log('\n FLOATING CODE HAS RUN ' + num);
return (
<>
<button onClick={add}>{num}</button>
<div>{log}</div>
</>
);
};
export default App;
Thanks
Also I saw What is the difference between useEffect and code in the body of a functional component? to try and answer my first question, but it didn't explain about declarations, only about the functional difference.
In response to your questions:
The code in the body of a functional component executes every time the component is rendered (your console.log('\n FLOATING CODE HAS RUN ' + num);) In contrast, code in a useEffect fires on the initial render, and then on every render during which the value of one of the elements in the dependency array has changed. In your example, it should run on the first render, and then every time setNum causes the value of num to change.
The reason for it executing twice is difficult to determine, but it may not be an issue. It is likely being caused by internal React mechanisms such as 'strict mode', and will not occur in the production build.
No, setLog should not necessarily execute before the console.log. React setState functions are not guaranteed to execute in sequential order. They should not be regarded as synchronous. You have to take this into consideration when designing your component. This is why, when setting a state that is based on a previous state, you need to pass a callback to the 'set' function with the previous state as a parameter. With that in mind, your const add = () => { setNum(num + 1); }; line is incorrect. It should instead be const add = () => { setNum(prevNum => prevNum + 1); };

useEffect not producing data in console.log in sequence

i am a begginer to react i am learning the useEffect hook currently i am a little bit confused over the sequence at which the data in console.log is printed , please explain it in steps , thanks
initailly i see b and c printed but then i see a ,b ,c after each second why is that ?
code
const [count, setCount] = useState(0);
const tick = () => {
setCount(count + 1)
}
useEffect(() => {
console.log("b")
const intervel = setInterval(tick, 1000)
console.log("c")
return () => {
console.log("a")
clearInterval(intervel)
}
}, [count]);
The function inside which you print a is called a cleanup function.
React calls that function before applying the next effects. That is why you see afterwards a printed each time count is changed. It is a cleanup being called from the previous render before applying effect for this render. From the docs:
When exactly does React clean up an effect? React performs the cleanup
when the component unmounts. However, as we learned earlier, effects
run for every render1 and not just once. This is why React also cleans
up effects from the previous render before running the effects next
time. We’ll discuss why this helps avoid bugs and how to opt out of
this behavior in case it creates performance issues later below.
1 That part of docs didn't talk about dependencies yet, hence it mentions effects running on each render.
The function you are returning at the end of useEffect()
return () => {
console.log("a")
clearInterval(intervel)
}
is not run the first time the component renders. That is called a cleanup function. You are returning that anonymous function to the top of the execution queue on the next render.
Knowing that, we can see that your render cycle will look something like this
Render 1:
console.log("b")
wait 1 second
console.log("c")
return console.log("a") to next render
Render 2:
console.log("a")
console.log("b")
wait 1 second
console.log("c")
return console.log("a") to next render
you set that useEffect depend on [count]
it means that you ask useEffect to recall everytime count change and call returned function (
return ()=>{
console.log("a")
clearInterval(intervel)
}
) after count change so
first component render
after render useEffect call this function
console.log("b")
const intervel = setInterval(tick, 1000)
console.log("c")
but tick change count every 1s and useEffect depend on it so after 1s
useEffect will clear because count changed so interval will stop
and will call again
so it will create a new interval and start again again again

My useEffect effect is running but I'm not sure why - dependency array value (seemingly) isn't changing

I'm learning about React hooks. One task to practice using the useRef and useEffect hooks was to build a "click counting" game. The game has a timer (which is powered by useEffect and setIinterval) that counts down from 10, and a state variable counts how many times you are able to click in that set amount of time.
I wanted to above and beyond and keep exploring so I wanted to add a button that would "reset" the game. I found that I had to add a state value to track whether the game is "active" or not (the game is not active when the countdown timer reaches 0). In order for the reset functionality to work I had to list this state value (called gameIsActive) in the useEffect dependency array. When the countdown timer reaches zero, the gameIsActive variable is switched from its default value of true to false, and clicking the reset button toggles it back to true, as well as resetting the other relevant state values (click count goes back to zero, timer goes back to 10, in this case).
What I'm struggling to understand is why this works. From the React docs on useEffect it would seem that adding gameIsActive to the dependency array should keep the effect from running, because during the game the value of gameIsActive does not change... The relevant wording in the docs:
In the example above, we pass [count] as the second argument. What does this mean? If the count is 5, and then our component re-renders with count still equal to 5, React will compare [5] from the previous render and [5] from the next render. Because all items in the array are the same (5 === 5), React would skip the effect. That’s our optimization.
When we render with count updated to 6, React will compare the items in the [5] array from the previous render to items in the [6] array from the next render. This time, React will re-apply the effect because 5 !== 6. If there are multiple items in the array, React will re-run the effect even if just one of them is different.
import React, { useState, useEffect, useRef } from "react";
import ReactDOM from "react-dom";
import "./styles.css";
function CounterGame() {
const [clickCount, setClickCount] = useState(0);
const [timeRemaining, setTimeRemaining] = useState(10);
const [gameIsActive, setGameIsActive] = useState(true);
const id = useRef(null);
const handleClick = () => {
setClickCount((clickCount) => clickCount + 1);
};
const handleReset = () => {
setTimeRemaining(10);
setClickCount(0);
setGameIsActive(true);
};
const clearInterval = () => {
window.clearInterval(id.current);
};
useEffect(() => {
id.current = window.setInterval(() => {
setTimeRemaining((timeRemaining) => timeRemaining - 1);
}, 1000);
return clearInterval;
// If gameIsActive is ommitted from the dependency array
// countdown timer will not restart when game is "reset"
}, [gameIsActive]);
useEffect(() => {
if (timeRemaining === 0) {
clearInterval();
}
setGameIsActive(false);
}, [timeRemaining]);
return (
<div className="App">
<h3>
Time remaining (secs):
{timeRemaining}
</h3>
<h3>
Click Count:
{clickCount}
</h3>
<button onClick={handleClick} disabled={!timeRemaining}>
Click Me
</button>
<button onClick={handleReset}>Reset Game</button>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<CounterGame />, rootElement);
If I exclude the gameIsActive from the dependency array on the first useEffect hook, the reset will not work if the counter hits zero. I'm operating on the guess that this is because when the timer hits zero, I clear the interval, and never re-instantiate it. Adding the gameIsActive state seemed necessary to trigger the effect to set another interval, so it makes sense when I go from false (when the timer hits zero) back to true (when I reset the "game"). But why does the effect run every time the timer ticks? I'm especially confused given I had to use the useRef hook to persist the interval ID from render to render, and that logic is occurring in the same hook.
Here is a link to a working codesandbox of the issue - you'll see that removing the gameIsActive value in the first useEffect's dependency array will cause the game to no longer work after the timer hits zero (although things will reset and start counting down if you click reset BEFORE the timer hits zero).
So what is currently happening:
Whenever the value of gameIsActive changes, the useEffect hook is executed. This means, that it is actually executed on the rising edge and the falling edge of the value change.
Now, as soon as your game starts, the gameIsActive value is initialized with true. However, as soon as there is the first game tick, you set the value to false. What is happening in the background is, that the interval gets cleared and initialized once again (it gets cleared due to the cleanup function you return in the hook) but you will not notice this, as the counter is unaffected by this.
As soon as the counter ticks to zero, you stop the interval in your second hook. Other than that, nothing happens as the gameIsActive state is already false.
Now, as you execute the reset function, you change the value of gameIsActive, leading to a state update and the execution of the useEffect hook. And again, you then instantly set the game gameIsActive value back to false.
This solution is not optimal. What you rather want to do is to define a dedicated startGame function, which will initialize the interval. You can then call this function once the game starts up, using a useEffect hook without any dependencies.
When you reset the game, you would then simply run the startGame function to restart the timer.
If you do it this way, you also don't ned to return a function from the useEffect hook. Instead, (to avoid concurrent intervals running) you would want to execute the clearInterval function as a first statement in your startGame function, to clear any potentially existing intervals.
It works because on the first tick of the timer -- it hits your useEffect (the time isnt 0) but it sets gameIsActive to false. Once you click reset it sets it back to true -- which triggers the useEffect to restart the timer. and so on and so forth.
Check out what happens when you comment out setGameIsActive(false); and you try and reset it -- notice it doesnt reset?

Trying to understand exactly what's going on when I use useEffect

I'm still pretty new to React and trying to wrap my head around hooks. I'm messing with a very simple counter example right now, using console.log() to try to understand what's going on behind the scenes:
import React, { useState, useEffect } from 'react'
const Counter = () => {
const [ count, updateCount ] = useState(0)
console.log(count)
useEffect(() => {
updateCount(10)
console.log(count)
}, [count])
return (
<div>
<h1>Counter</h1>
<div>
<button onClick={() => updateCount(count-1)}>-</button>
<span>{count}</span>
<button onClick={() => updateCount(count+1)}>+</button>
</div>
</div>
)
}
When I load this page, I see 0, 0, 10, 10, 10 logged to the console. So as I understand it, this is what's happening:
The component is rendered, setting the default value of count to 0.
0 is then logged to the console, as per line 5
useEffect always executes when the component is first rendered, regardless of its dependencies (right?), so it then executes updateCount(10)
Before the state is actually updated from updateCount(10), 0 is logged to the console as per line 9
count updates because of updateCount(10). Because the state has changed, the component re-renders. 10 is logged to the console again as per line 5
Since count is listed as a dependency of useEffect, and count has changed, useEffect executes again.
updateCount(10) is called again, though it shouldn't do anything since count is already 10.
10 is logged to the console again, as per line 9.
This leaves one more 10 being logged to the console, from line 5. I'm confused why this is being executed again. The last time useEffect was called, it set count to 10 again, but since count was already set to ten, the component shouldn't have a need to re-render, so why was line 5 being executed again? Or is the component re-rendered every time an update to
the state is called, regardless of whether the properties of the state actually change or not?
The reason you are seeing the third 10 logged out is because of a quirk in reacts implementation details of concurrency.
https://github.com/facebook/react/issues/17474
Normally if setState is strictly equal the component won't re render:
updateCount(prevState => prevState)
But if there is any ambiguity on state values (because of concurrency) react needs to do a "second" render to be certain that the state value is the same.
It definitely is an interesting quirk, and shows that you should never rely on the amount of times a component will render.

React "interval" has always the same state

I am using react 16.10 with typescript.
I have this code:
const [state, setState] = useState<State>({
test: 1
});
//On component mount we start our interval
useEffect(() => {
const timerID = setInterval(timer, 5000); //every 5 seconds
return function cleanup() {
//When we leave component we stop the timer
clearInterval(timerID);
};
}, []);
function timer() {
if(state.test === 3){
//HE WILL NEVER ENTER THIS CODE FUNCTION
}
setState({...state, test: 3}); // Next time we should have the value 3, BUT IT HAS NEVER THIS VALUE?!?!
}
return (
<>
<span>The output is {state.test}</span> //First it is showing 1, after 5 seconds 3. Working great
</>
);
I am changing the value of test to the number 3 in the interval "timer". setState is working fine. I can see the value in my component, seeing the number switching from 1 to 3.
But in the timer function the value is never changed. It has every time the default value of 1.
What I am doing wrong?
You need to add dependency to useEffect
//On component mount we start our interval
useEffect(() => {
const timerID = setInterval(timer, 5000); //every 5 seconds
return function cleanup() {
//When we leave component we stop the timer
clearInterval(timerID);
};
}, [state.test]); // <- here add dependency
Reason
Your effect function is called only once when component is mounted and it stored timer functions reference. now when you state changes your timer function is also updated outside but not inside of useEffect.
useEffect still uses old reference when state was 1 so inside it State always going to be 1 for that referred timer function
Now when you pass state.test as dependency. when state get changed your effect will updated and it now start using new timer function which has new state.
So now, you can have updated state in your timer function. and your condition can evaluate correctly.
if any doubts please comment.
You are not doing anything wrong, your useEffect() has a completely different value in memory and without knowing this behavior about useEffect() you have nothing in there telling useEffect() to stop looking at that old value and start looking at the new value. As Hardik wrote, your useEffect() is called only once, so you still have that old value that was originally called in there and useEffect has no idea that your timer has changed since. It will be referencing that original value forever.
What you can do is completely remove the empty array as the second argument and you will notice the difference in behavior.
Using a direct reference to the variable you are using in your state as suggested by Hardik seems to be the way to go.
So again, useEffect() is not being called a second time and as a result, nothing inside it is being ran again so it all in stale reference.
One of the tips the facebook team gives to mitigate this bug:
When you have a useEffect function that references a state, props, or context values, add them to your dependency list. In other words, if you have a props called trackId, you would want to do something like this:
useEffect(() => {
trackId
}, [trackId]);
I see a couple of potential issues, first of all you need to be calling this.state and this.setState. I'd guess state is undefined but this.state won't be. You also don't need to spread your state in your setState function, this.setState({ test: 3}); is good enough, the setState function does this for you.
Secondly you need to update state for every change, it looks like you're only updating if the test value is 3, I'm surprised it's ever 3 with this implementation

Resources