Why is useEffect running before component re-render? - reactjs

I am new to react and this is a very simple counter that increments value by 5, I learnt that useEffect is executed after every component re-render/dependency variable change. But I found that useEffect (i.e alert) is appearing before the value in the h1 changes
import { useEffect, useState } from "react";
export default function App() {
const [number, setNumber] = useState(0);
let prev = 0;
useEffect(() => {
if (number !== 0) {
alert("Number changed to " + number);
}
}, [prev, number]);
console.log(prev);
return (
<>
<h1>{number}</h1>
<button
onClick={() => {
setNumber((n) => {
prev = n;
return n + 5;
});
}}>
+5
</button>
</>
);
}
Expected Outcome: alert happens after h1 value increments by 5
Current Result: alert comes first and h1 value increments after closing the alert

This is when useEffect runs:
useEffect(() => {
/* Runs at every (First time, and anytime the component is rendered) render! */
})
useEffect(() => {
/* Runs only when the component is rendered for the first time! */
}, [])
useEffect(() => {
/* Runs when the component is rendered for the first time and whenever the someDependency is updated! */
}, [someDependency])
Therefore, in your case, it runs when the component is rendered for the first time, when the number changes, and when the prev changes. Also, do not change prev the way you are doing it right now, it will cause an infinite loop!

useEffect runs basically like componentDidMount,
so it runs first time after the component mounted and then after every re-render

Related

How to use useEffect/state/variables properly without user interaction?

My goal is to set up a game loop but a simple test isn't working as expected. In the following component, I am trying the useEffect hook to increment food. I expect to see "Food: 1". Instead I see "Food: 0". When I inspect the component with the dev tools, I can see that food is 2. I've discovered that the component mounts, increments food, unmounts, mounts again and increments food once more.
I have two questions:
Can I do something about the double mount? (like prevent it or wait until the final mount with a nested component perhaps?)
Why does the displayed food count still equal zero? Is it because game inside <span>Food: {game.food}</span> still refers to the initial instance? If so, how do I get the latest instance?
Component:
import React from "react";
class Game {
food = 0;
}
export default function App() {
const [game, setGame] = React.useState(new Game());
React.useEffect(() => {
setGame((game) => {
game.food += 1;
return game;
});
});
return <span>Food: {game.food}</span>;
}
Don't Mutate State Objects
React uses reference comparisons and expects the reference of the root state object to change if any data within it has changed.
For Example:
// DON'T
setGame((game) => {
// mutate and return same object
game.food += 1;
return game;
});
// DO
setGame((current) => {
// create new object with updated food value
return {
...current,
food: current.food + 1
};
});
Using the same reference will cause components to not update as expected.
useEffect Dependency Array
A useEffect without a dependency array will trigger every time the component renders.
If you wish for the useEffect to only trigger on mount provide an empty dependency array.
For Example:
// Every Render
useEffect(() => {
alert('I trigger every render');
});
// On Mount
useEffect(() => {
alert('I trigger on mount');
}, []);
// Everytime the reference for game changes
useEffect(() => {
alert('I trigger everytime the game state is update');
}, [game]);
Conclusion
"Mount twice" probably you are using react 18 and have strict mode enabled. It will trigger useEffect twice in dev mode from docs
If you want to update the view, you should make the reference of the game variable changes (instead of changing its attrs).
Solution
const initialGame = {
food: 0
}
export default function App() {
const [game, setGame] = React.useState(initialGame);
React.useEffect(() => {
setGame((game) => {
game.food += 1;
return {...game};
});
}, []);
return <span>Food: {game.food}</span>;
}
No you should not useEffect as a loop, its execution depends on your component states and its parent component, so this leaves 3 solutions 1st while loop, 2nd requestAnimationFrame and 3rd setInterval. while loop is discouraged because it will block event loop and canceling/stopping can be tedious.
double mount ? i think its react double checking function, which does this only dev mode. Once you switch to requestAnimationFrame you won't be having that issue.
use tried mutate state and react doesn't recognizes this so it doesn't re render. solution: return new object.
updating states
useEffect(() => {
setGame((current) => {
const newState = { ...current, food: current.food + 1 }
return newState
})
}, [])
using setInterval to act as loop
useEffect(() => {
const id = setInterval(() => setCount((count) => count + 1), 1000)
return () => clearInterval(id)
}, [])
using requestAnimationFrame to act as loop
// credit: https://css-tricks.com/using-requestanimationframe-with-react-hooks/
const requestRef = React.useRef()
const animate = (time) => {
setCount((count) => count + 1)
requestRef.current = requestAnimationFrame(animate)
}
useEffect(() => {
requestRef.current = requestAnimationFrame(animate)
return () => cancelAnimationFrame(requestRef.current)
}, []) // Make sure the effect runs only once

Using useSelector from redux and useEffect from react, but the value doesn't change even if I renew the value

There are two components, I expected them to have same effect!However, they are not! For the first code, I have a var n, and I increace the n using useSelector from redux, I can see the n is cahnged when the selector is working. But there are still display the unchanged n in the return, I also use useEffect, if n is changed, renew the component. But still not work. However, the second code is work. The second code I also use useSlector to return the n, and that is work. Why the first code dosen't work, even if I use useEffect???
My Question
In my first code, I kept updating n in useSeletor, I thought that the state of n changed, it would trigger useEffect() (useEffect subscribed to [n]) to update the component, but it doesn't seem to be. Why is this happening, I have subscribed to n with useEffect, doesn't every change you make update the component?
//First code
const TotalCompleteItems = () => {
let n = 0
let [state, setState] = useState
useSelector(state => {
n=0
state.todos.forEach(element => {
(element.completed===true) && (n++);
console.log(n)
})
})
useEffect(() => {
},[n])
return <h4 className='mt-3'>Total Complete Items: {n}</h4>;
};
export default TotalCompleteItems;
//Second code
const TotalCompleteItems = () => {
let n = useSelector(state => {
let num=0
state.todos.forEach(element => {
(element.completed===true) && num++;
})
return num
}
)
return <h4 className='mt-3'>Total Complete Items: { n }</h4> };
export default TotalCompleteItems;
My Question In my first code, I kept updating n in useSeletor, I
thought that the state of n changed, it would trigger useEffect()
(useEffect subscribed to [n]) to update the component, but it doesn't
seem to be. Why is this happening, I have subscribed to n with
useEffect, doesn't every change you make update the component?
This is a little bit of the tail wagging the dog. You are mutating n, but simply mutating n isn't enough to trigger the component to rerender and call the useEffect hook at the end of the render cycle to check if any dependencies updated. This also wouldn't work anyway because n is always redeclared at the start of the render cycle.
If you are just wanting to compute a total completed value then just use an array.reduce over the todos array and return a computed count.
const TotalCompleteItems = () => {
const n = useSelector(state => {
return state.todos.reduce(
(total, todo) => total + Number(!!todo.completed),
0
);
});
return <h4 className='mt-3'>Total Complete Items: {n}</h4>;
};
or more simply use an array.filter over the todos array and return the length.
const TotalCompleteItems = () => {
const n = useSelector(state => {
return state.todos.filter(todo => todo.completed).length;
});
return <h4 className='mt-3'>Total Complete Items: {n}</h4>;
};

React: Re-render strange behavior

Example:
https://codesandbox.io/s/friendly-lumiere-srnyu?file=/src/App.js
I found the console is print 3 times and the last time was state:2,version:1, but the view is keep state:2,version:0.
I can't understand what happen in React.
I thought that "React would not call re-render if set the same value (primitive value)", But this example overthrew my idea.
The initial value of v is true (line 12), it will call third re-render if call setV(true)(line 16), But not update on the View in this re-render call.
Just call 1 times if comment lines 8 to 10.
import React, { useEffect, useState } from "react";
let version = 0;
export default () => {
const [state, setState] = useState(1);
// useEffect(() => {
// setState(2);
// }, []);
const [v, setV] = useState(true);
useEffect(() => {
setTimeout(() => {
version = 1;
setV(true);
}, 1000);
}, []);
console.log(`state:${state},version:${version}`);
return (
<div>
{`state:${state},version:${version}`}
</div>
);
};
version is not part of the state, the state being what React uses to decide if it needs to re-render a component.

React state not updating inside setInterval

I'm trying to learn React with some simple projects and can't seem to get my head around the following code, so would appreciate an explanation.
This snippet from a simple countdown function works fine; however, when I console.log, the setTime appears to correctly update the value of 'seconds', but when I console.log(time) immediately after it gives me the original value of 3. Why is this?
Bonus question - when the function startCountdown is called there is a delay in the correct time values appearing in my JSX, which I assume is down to the variable 'seconds' being populated and the start of the setInterval function, so I don't get a smooth and accurate start to the countdown. Is there a way around this?
const [ time, setTime ] = useState(3);
const [ clockActive, setClockActive ] = useState(false);
function startCountdown() {
let seconds = time * 60;
setClockActive(true);
let interval = setInterval(() => {
setTime(seconds--);
console.log(seconds); // Returns 179
console.log(time); // Returns 3
if(seconds < 0 ) {
clearInterval(interval);
}
}, 1000)
};
Update:
The reason you are not seeing the correct value in your function is the way that setState happens(setTime). When you call setState, it batches the calls and performs them when it wants to in the background. So you cannot call setState then immediately expect to be able to use its value inside of the function.
You can Take the console.log out of the function and put it in the render method and you will see the correct value.
Or you can try useEffect like this.
//This means that anytime you use setTime and the component is updated, print the current value of time. Only do this when time changes.
useEffect(()=>{
console.log(time);
},[time]);
Every time you setState you are rerendering the component which causes a havoc on state. So every second inside of your setInterval, you are re-rendering the component and starting it all over again ontop of what you already having running. To fix this, you need to use useEffect and pass in the state variables that you are using. I did an example for you here:
https://codesandbox.io/s/jolly-keller-qfwmx?file=/src/clock.js
import React, { useState, useEffect } from "react";
const Clock = (props) => {
const [time, setTime] = useState(3);
const [clockActive, setClockActive] = useState(false);
useEffect(() => {
let seconds = 60;
setClockActive(true);
const interval = setInterval(() => {
setTime((time) => time - 1);
}, 1000);
if (time <= 0) {
setClockActive(false);
clearInterval(interval);
}
return () => {
setClockActive(false);
clearInterval(interval);
};
}, [time, clockActive]);
return (
<>
{`Clock is currently ${clockActive === true ? "Active" : "Not Active"}`}
<br />
{`Time is ${time}`}
</>
);
};
export default Clock;

Does clearing timeout/interval have to be inside `useEffect` react hook?

I'm wondering what is the correct way and best practice to clear timeouts/intervals when using React hooks. For example I have the following code:
import React, { useState, useEffect, useRef } from 'react';
const Test = () => {
const id = useRef(null)
const [count, setCount] = useState(5)
const [timesClicked, setTimesClicked] = useState(0)
if (!count) {
clearInterval(id.current)
}
useEffect(() => {
id.current = setInterval(() => {
setCount(count => count -1)
}, 1000)
return () => {
clearInterval(id.current)
}
}, [])
const onClick = () => setTimesClicked(timesClicked => timesClicked + 1)
return (
<div>countdown: {count >= 0 ? count : 0}
<hr />
Clicked so far: {timesClicked}
{count >= 0 && <button onClick={onClick}>Click</button>}
</div>
)
}
When count equals 0 the interval is cleared in the body of the Test function. In most of the examples I've seen on the Internet interval is cleared inside useEffect, is this mandatory?
You must be sure to clear all intervals before your component gets unmounted.
Intervals never disappear automatically when components get unmounted and to clear them, clearInterval is often called inside useEffect(() => {}, []).
The function retured in useEffect(() => {}, []) gets called when the compoment is unmounted.
return () => {
clearInterval(id.current)
}
You can see that intervals set inside a component never disappears automatically by checking this sandbox link. https://codesandbox.io/s/cool-water-oij8s
Intervals remain forever unless clearInterval is called.
setInterval is a function which is executed repeatedly and it returns an id of the interval. When you call clearInterval with this id, you stop that function from repeating. It's not mandatory to do it inside a certain function, you need to clear it when you no longer want that function to be called subsequently. You can call it in the function you return as a result of useEffect, if that's what you need.

Resources