How to use an updated value of a variable (declared at component scope) in each 'useEffect'?
import React, { useState, useEffect } from 'react';
export default function Count() {
const [count, setCount] = useState(0);
let a = 10;
useEffect(() => {
console.log('after 1st render', a);
a++;
console.log(a);
return () => { console.log('cleanup - on unmount.') }
}, [a]);
useEffect(() => {
console.log('only when count changes', a);
a++;
return () => { console.log('count cleanup', a) }
}, [count, a]);
return <div>
<p>Count : {count}</p>
<button onClick={() => { console.log('at global', a); setCount(count + 1) }}>Click</button>
</div>
}
output
after 1st render 10
11
only when count changes 11
at global 12
count cleanup 12
only when count changes 10
Now, What I don't understand from this output is the last line which outputs the value of 'a' as 10.
Every time a useEffect gets called, it creates a new copy of the function provided, and it also calls the cleanUp function right?.
when I click the button the count changes, the previous cleanUp gets called and clean that 'useEffect' behaviour which sets the value of 'a' from 11 to 12, and then the current 'useEffect' called with value 10. it should print the value 11. ? can anyone please clarify this.
On every render, all Count body executed, therefore changing state via button click will result in calling let a = 10 and resetting the value of a.
In other words, the local variable a lifetime is until the next render.
To get desired behavior, try using a reference with useRef.
useRef returns a mutable ref object whose .current property is
initialized to the passed argument (initialValue). The returned object
will persist for the full lifetime of the component.
const [count, setCount] = useState(0);
const aRef = useRef(10);
let a = 10;
useEffect(() => {
console.log("after 1st render", a);
a++;
aRef.current++;
console.log(a);
return () => {
console.log("cleanup - on unmount.");
};
}, [a]);
useEffect(() => {
console.log("only when count changes", a);
console.log("only when count changes - ref", aRef.current);
a++;
aRef.current++;
return () => {
console.log("count cleanup", a);
};
}, [count, a]);
Will result:
only when count changes
10
only when count changes - ref
12
Read more at uses of useEffect
Related
Hello i am struggling to set dynamic time for settimeout function in react js.
i have long string of key value pair of time and message. i wants to display each message for specific time and loop through whole list.
here is what i am trying, but not working.
const [timer, setTimer] = useState(0)
const [time, setTime] = useState(5000)// this is default value to start which need to update with str time value
const str=[{name:"rammy", time:1000},
{name:"james", time:4000},
{name:"crown", time:2000}]
useEffect(()=>{
const getTime= str[timer].time
setTime(getTime)
},[timer])
//when timer change it should update update time state which will be used to update time for time settime out
function increment() {
useEffect(()=>{
setTimeout(() => {
setTimer((ele)=>ele+1)
}, time);
},[timer])
} // above code is for increment time state on each iteration
function ButtonHandle(){
//setRealString(itr)
increment()
} //button handler for start timer
First of all, you can't put hooks inside functions (other than your component functions). https://reactjs.org/docs/hooks-rules.html
So take the useEffect out of increment()
useEffect(()=>{
increment()
},[timer])
function increment() {
setTimeout(() => {
setTimer((ele)=>ele+1)
}, time);
}
But you also need to clear the timeout. We can return the timeout function to reference it, and clear the time out with a return inside useEffect. Clearing timeouts and intervals in react
useEffect(()=>{
const myTimeout = increment()
return () => {
clearTimeout(myTimeout)
}
},[timer])
function increment() {
return setTimeout(() => {
setTimer((ele) => ele + 1);
}, time);
}
Then we can combine the useEffects which both have a dependancy array of [timer].
useEffect(() => {
const getTime = str[timer].time;
setTime(getTime);
const myTimeout = increment();
return () => {
clearTimeout(myTimeout);
};
}, [timer]);
You don't need to use useEffect to do it. You misunderstood the useEffect usage, it's a react hook to you implement side-effects and you can't use react-hooks inside a function, it should be in the component scope.
I can increment directly from the ButtonHandle function.
// On the index state is necessary in this implementation
const [index, setIndex] = useState(-1)
const guys=[
{name: "rammy", time:1000},
{name: "james", time:4000},
{name: "crown", time:2000}
]
// useCallback memoize the increment function so that it won't
// change the instance and you can use it in the useEffect
// dependency array
const increment = useCallback(() => {
setIndex((i) => i+1)
}, [])
useEffect(() => {
// change the state if the timer is greater than -1
if (index !== -1) {
if (index >= guys.length) {
setIndex(-1);
} else {
setTimeout(() => {
increment();
}, guys[index].time); // <-- you get the time from your array
}
}
}, [index, increment]);
function handleClick(){
//setRealString(itr)
increment()
}
Even though I helped you, I don't know what you're trying to do. This implementation sounds like a code smell. We can help you better if you explain the solution you're trying to do instead of just the peace of code.
You don't need to set the time state, as you already have the time in the array; avoiding unnecessary state changes is good.
I have this code here:
export default function App() {
const [count, setCount] = useState(0);
const [step, setStep] = useState(0);
const reff = useRef(0);
useEffect(() => {
const id = setInterval(() => {
setCount((c) => {
console.log({ step }, "reff.current before incr ", reff.current);
reff.current = reff.current + step;
console.log("ref.curret after incr ", reff.current);
return c + step;
});
}, 6000);
return () => clearInterval(id);
}, [step]);
return (
<>
<h1>Count:{count}</h1>
<h1>Step: {step}</h1>
<input
type="button"
onClick={(e) => setStep((prevStep) =>(prevStep + 1))}
value="+ step"
/>
</>
);
}
You can view it here: https://codesandbox.io/s/gracious-roentgen-31ntb?file=/src/App.js:0-817
Note that I'm incrementing ref inside setCount callback
When the component loads a setInterval is started. If I trigger the useEffect again by incrementing step before 6 seconds pass, it clears the setInterval and creates a new one.
And now if I don't increment step again and wait for 6 seconds, I see step: 1 reff.current before incr, 0 for the first time the setInterval callback is called.
After incrementing in the next line(reff.current = reff.current + step) I see "ref.curret after incr ", 1
When the setInterval callback is called again after 6 seconds, I see
step: 1 reff.current before incr, 2 //how did this become 2
I don't understand how the value of reff.current is 2.
This only happens when I increment the step(which clears the first interval). If I set the initial step to 1 and don't increment it, I see expected values.. Just checked again. It doesn't work as expected.
I can't understand why the value of reff.current is 2 when the setInterval callback is called the second time.
Go to the sandbox link and
Click on the +step button once
Open the console and see
log 1:{step: 1} "reff.current before incr "0
log 2: ref.curret after incr 1
//second time callback is called
log 3: {step: 1} "reff.current before incr "2 //this should be 1
log 4: ref.curret after incr 3```
Your problem happens because you are performing a side effect (updating reff.current) inside the updater function passed to the setCount.
The beta react docs say that:
Updater functions run during rendering, so updater functions must be
pure and only return the result. Don’t try to set state from inside of
them or run other side effects. In Strict Mode, React will run each
updater function twice (but discard the second result) to help you
find mistakes.
You are running your app in StrictMode. To verify it comment it:
const rootElement = document.getElementById('root');
ReactDOM.render(
// <StrictMode>
<App />,
// </StrictMode>,
rootElement
);
and the problem doesn't happen anymore.
Can you please try out this code below,
Not sure about the exact reason why the value was incremented, but using it directly instead of returning from a function, did work as expected. Also it works for further step increment.
import { useEffect, useRef, useState } from "react";
import "./styles.css";
export default function App() {
const [count, setCount] = useState(0);
const [step, setStep] = useState(0);
const reff = useRef(0);
useEffect(() => {
const id = setInterval(() => {
console.log({ step }, "reff.current before incr ", reff.current);
debugger;
// ------ modified here
let newVal = reff.current + step;
reff.current = newVal;
console.log("ref.curret after incr ", reff.current);
setCount(newVal);
// ------ modified here
}, 6000);
return () => clearInterval(id);
}, [step]);
return (
<>
<h1>Count:{count}</h1>
<h1>Step: {step}</h1>
<input
type="button"
onClick={(e) => {
e.preventDefault();
setStep((prevStep) => {
console.log(prevStep);
return Number(prevStep) + 1;
});
}}
value="+ step"
/>
</>
);
}
The image of the log
I am currently refactoring a react app from setState to hooks. I can't understand why the state variables aren't changed. Here is an example:
import React, { useState, useEffect } from 'react';
function Hook() {
const [num, setNum] = useState(1);
useEffect(() => {
window.addEventListener("mousemove", logNum);
}, []);
const logNum = () => {
console.log(num);
}
const handleToggle = () => {
if (num == 1) {
console.log('setting num to 2');
setNum(2);
} else {
console.log('setting num to 1');
setNum(1);
}
}
return (
<div>
<button onClick={handleToggle}>TOGGLE BOOL</button>
</div>
);
}
export default Hook;
When i click the button, I was expecting the output to be something like:
// 1
// setting num to 2
// 2
// setting num to 1
// 1
But the output look like this:
Why is the updated num variable not logged?
Shouldn't the logNum() function always point to the current value of the state?
That's why effect dependencies have to be exhaustive. Don't lie about dependencies.
logNum closures over num, so on every rerender there is a new num variable containing the new value, and a new logNum function logging that value. Your effect however gets initialized only once, thus it only knows the first logNum. Therefore, you have to add logNum as a dependency, so that the effect gets updated whenever num and thus logNum changes:
useEffect(() => {
window.addEventListener("mousemove", logNum);
}, [logNum]);
You'll notice that your effect does not correctly clean up, you should add a removeEventListener too.
return () => window.removeEventListener("mousemove", logNum);
Now if you debug this piece of code, you'll notice that the effect triggers on every rerender. That is because a new logNum function gets created on every rerender, no matter wether num changes or not. To prevent that, you can use useCallback to make the logNum reference stable:
const logNum = useCallback(() => console.log(num), [num]);
An alternative to all of this would be to use a reference to the current state:
const actualNum = useRef(num);
// that works no matter when and how this is executed
console.log(actualNum.current);
So I'm getting into Reactjs with very basic component.
I'm logging out the same state from different functions, but what I'm seeing is the different values.
import React, { useState, useEffect, useRef } from "react";
const Test = props => {
const [count, setCount] = useState(0);
useEffect(()=>{
setInterval(() => {
console.log("count in interval is:", count);
}, 1000);
},[props]);
function btnClick() {
const newCount = count + 1;
setCount(newCount);
console.log("count changed to: ", newCount);
}
return (
<div>
count is {count}
<br></br>
<button onClick={btnClick}>+</button>
</div>
);
};
export default Test;
Output after some clicks and wait, log is:
Test.js:8 count in interval is: 0
Test.js:15 count changed to: 1
Test.js:15 count changed to: 2
Test.js:15 count changed to: 3
Test.js:15 count changed to: 4
(8 rows) Test.js:8 count in interval is: 0
I expect the "count" to be the same in both functions.
Can any one explain this?
Thank so much.
Test only has one setInterval function where count is always 0. Since it's only created during initial render.
It never had another setInterval created because the effect never got triggered with [props] as the dependency.
To have setInterval's count change on every re-render:
Remove the dependency
Return a clean-up function inside the effect
useEffect(
() => {
const t = setInterval(() => {
console.log("count in interval is:", count);
}, 1000);
return () => clearInterval(t); // cleanup on every re-render
}
// no dependency: effect runs on every re-render
);
But the above code will have a warning:
"missing count dependency"
So simply add count as dependency to only run the effect when count changes.
useEffect(
() => {
const t = setInterval(() => {
console.log("count in interval is:", count);
}, 1000);
return () => clearInterval(t); // cleanup "old" setInterval
}
, [count] // ony run effect every time count changes
);
The value of count doesn't change, this is the expected behavior, though not an obvious one.
See, you even declare count as a const count, it is immutable. What is happening instead is that during the first render count gets assigned value of 0. The value of count never changes, what happens instead is that component Test is called each time you change the state, and function useState assigns different values to the constant count, which is new constant every time.
So during the first render the value of const count gets captured by closure inside your function that is called by setInterval and the value stays 0 forever.
I would like to know the difference between the following two versions of code. Both versions do the same.
1) Here just the counter variable is used to get the current value
const Counter = () => {
const [counter, setCounter] = useState(0);
return <button onClick={() => setCounter(counter + 1)}>{counter}</button>;
}
2) This version passes a function to setCounter
const Counter = () => {
const [counter, setCounter] = useState(0);
return <button onClick={() => setCounter(c => c + 1)}>{counter}</button>;
}
The official documentation says:
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.
So what's wrong with the first option? Are there some pitfalls?
With the particular code in your example, you have the previous value in hand, so there isn't much difference. But sometimes you don't. For example, suppose you wanted to have a memoized callback function. Due to the memoization, the value of counter gets locked in when the closure is created, and won't be up to date.
const Counter = () => {
const [counter, setCounter] = useState(0);
// The following function gets created just once, the first time Counter renders.
const onClick = useCallback(() => {
setCounter(c => c + 1); // This works as intended
//setCounter(counter + 1); // This would always set it to 1, never to 2 or more.
}, []); // You could of course add counter to this array, but then you don't get as much benefit from the memoization.
return <button onClick={onClick}>{counter}</button>;
}