On React hook component unmount, unable to get updated state variable value - reactjs

After clicking on "Click me" button my count value is updating. Now I have to Do Something if count is grater then 0 before my component unmount.
But I have noticed during debugger count value is always 0. While I was expecting count should be grater then 0 if I clicked multiple times.
Please help me, how to get updated value during component unmount. Thanks
import React, { useState, useEffect } from 'react';
function Example() {
const [count, setCount] = useState(0);
useEffect(() => {
//ComponentDidMount
return(()=>{
//componentWillUnmount
alert(count); //count 0
if(count){
//Do Something
}
})
},[]);
useEffect(() => {
document.title = `You clicked ${count} times`;
});
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}

You are forming a closure over the original value of count when you set up your cleanup function in the useEffect. That means that even as the value of count updates in the state, the value of count stays as 0 in the cleanup function.
To avoid this, you need to add count to the array of dependencies for your useEffect. That way, when count updates in state, and the component re-renders, the cleanup function also updates with the latest value of count.
useEffect(() => {
return (()=> {
alert(count); // this will now be latest value of count on unmount
if(count) {
// Do Something
}
})
}, [count]); // add is now a dependency of useEffect

Related

React: update state only once, when another state reaches certain value

Here's my code https://codesandbox.io/s/heuristic-cannon-lvse23?file=/src/App.js
import { useEffect, useState } from "react";
import "./styles.css";
function useAsyncState() {
const [status, setStatus] = useState("idle");
function trigger(value) {
setStatus(`sending ${value}`);
setTimeout(function () {
setStatus("done");
}, 1500);
}
return [status, trigger];
}
export default function App() {
const [value, setValue] = useState(0);
const [status, trigger] = useAsyncState();
useEffect(() => {
if (status === "done") {
setValue(value - 1);
}
}, [status, value]);
return (
<div className="App">
<p>
status: {status}
<button onClick={() => trigger(value)}>send & decrement</button>
</p>
<p>
value: {value}
<button onClick={() => setValue(value + 1)}>increment</button>
</p>
</div>
);
}
I have some kind of async action wrapped into a custom hook useAsyncState (simplified version of some real hook from my dependencies).
The problem is, I want to update value only once, when async action turns into done state.
However, changing state rerenders the component and since status is still done, value changes again, leading to endless loop of decrementing.
Also, action can be triggered again and I want to decrement value, when it's done again.
If I had an original Promise, I could do it in then callback, but I have only a hook like this.
Also, using useEffect delays value update to the next render, so it takes two renders to show decremeneted value. It's not a big deal, but it would be nice to do it in single render.
Is it ever possible to do it nice with such a custom hook? Or using raw Promises is the only way?
You can use a Functional Update instead:
If the new state is computed using the previous state, you can pass a function to setState. The function will receive the previous value, and return an updated value.
useEffect(() => {
if (status === "done") {
setValue((prevValue) => prevValue - 1);
}
}, [status]);

Why is useEffect running before component re-render?

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

useEffect() vs setTimeout() for side effects [duplicate]

This question already has answers here:
React - useState - why setTimeout function does not have latest state value?
(2 answers)
Closed 2 years ago.
I have a general question about useEffect() vs setTimeout().
import { useState, useEffect } from 'react';
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => console.log(count)); // effect
const handleClick = () => {
setCount(count + 1);
setTimeout(() => console.log(count), 5000); // log to console after 5 seconds
}
return (
<>
<p>You clicked {count} times</p>
<button onClick={handleClick}>Click Me</button>
</>
);
}
export default Counter;
In the above code,
setCount(count + 1);
setTimeout(() => console.log(count), 5000);
using setTimeout(), I am trying to log count after 5 seconds, by which time setCount(), although asynchronous, would have definitely finished.
Then why does that still print the previous value of count, while useEffect() code prints the updated value? Can someone tell me what I am missing.
A lot of things to say about this code:
import { useState, useEffect } from 'react';
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => console.log(count), [count]); // Add the dependencies array, the callback of use effect will be call only if a dependency is updated
const handleClick = () => {
setCount((count) => count + 1); // Use a callback which takes as argument the previous state, because if you click multiple times on the button, as setCount is asynchronous, it won't add 1 for each click
setTimeout(() => console.log(count), 5000); // log to console after 5 seconds
}
return (
<>
<p>You clicked {count} times</p>
<button onClick={handleClick}>Click Me</button>
</>
);
}
export default Counter;
I think that it is because the callback of setTimeout is evaluated before the njew count variable is computed, so it logs the previous value, where useEffect's callback is called when a dependency is updated, so when you increment count.

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.

With react hooks set state, in what cases I have to pass a function to set state?

here is my code:
function Tiker() {
var [count, setCount] = useState(0);
useEffect(() => {
var timerID = setInterval(_=>
setCount(count=>count+1)//setCount(count+1) wont work
, 1000);
return function cleanup() {
clearInterval(timerID);
};
}, []);
return <div>
this is ticker
<button onClick={() =>
setCount(count + 1)//setCount(count+1) does work
}>up </button>
{count}
</div>
}
By trial and error I discovered that if I use setCount from within setinterval callback, I have to pass a callback to the set state rather than just value.
its not the case if I call from onclick.
Why is that?
The problem is the second argument of useEffect
useEffect(() => {
var timerID = setInterval(_=>
setCount(count=>count+1)//setCount(count+1) wont work
, 1000);
return function cleanup() {
clearInterval(timerID);
};
}, []);
It is empty array ([]). It defines list of dependencies for hook. As it empty, it means that hook is not dependent from any value of state or props. So count variable is consumed on first call of useEffect and than stays stale.
To correct this you should either completely remove second argument of useEffect or make array contain [count].
Callback is working as it receives previous count value as first argument.
So correct code will look like
function Tiker() {
var [count, setCount] = useState(0);
useEffect(() => {
var timerID = setInterval(_=>
setCount(count + 1)
, 1000);
return function cleanup() {
clearInterval(timerID);
};
}, [count]); // Put variable that useHook depends on
return <div>
this is ticker
<button onClick={() =>
setCount(count + 1) //setCount(count+1) does work
}>up </button>
{count}
</div>
}
It appears that using a callback is the easiest way to change state when called from setInterval. as evident by Frodor comment above and from https://overreacted.io/making-setinterval-declarative-with-react-hooks/

Resources