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);
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.
Take the below code as an example.
import { useEffect, useState } from "react";
export default function Stopwatch(){
const [stopwatch, setStopwatch] = useState(0);
function updateStopwatch(){
console.log("Updating stopwatch from ", stopwatch, " to ", stopwatch+1);
setStopwatch(stopwatch + 1);
}
useEffect(() => {
const interval = setInterval(updateStopwatch, 1000);
return () => clearInterval(interval);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<div>
<h1>{stopwatch}</h1>
</div>
);
}
In this code, the function updateStopwatch will be invoked periodically without depending on any re-rendering of the component ( Although its invocation will cause a re-render ) and each time it will be invoked, it needs to retrieve the current up-to-date value of the state variable stopwatch, so it can update it accordingly. So that the stop watch will keep counting up.
However, what happens is that it only gets the value of stopwatch at the moment the function is declared. It's not really subscribed to a state variable but more like it just reads a constant once. And as a result the count show 1 and is stuck there.
So how can I make such a function get an up-to-date value of a state variable whenever it's invoked?
In your setState method, you can retrieve a prevState argument, which is always the right value, here is the way you could do your function based on your example :
function updateStopwatch(){
setStopwatch((prevState) => prevState + 1);
}
This is an issue of a stale closure over the initial stopwatch state value. Use a functional state update to correctly update from the previous state value instead of whatever value is closed over in callback scope at the time of execution. Move updateStopwatch declaration into the useEffect hook to remove it as an external dependency.
Example:
export default function Stopwatch(){
const [stopwatch, setStopwatch] = useState(0);
useEffect(() => {
function updateStopwatch(){
setStopwatch(stopwatch => stopwatch + 1);
}
const interval = setInterval(updateStopwatch, 1000);
return () => clearInterval(interval);
}, []);
return (
<div>
<h1>{stopwatch}</h1>
</div>
);
}
I am trying to run a function continuously until a condition is met, but in this test case, until the off button is pressed. My first issue is that the function does not stop when i press the off button.
let intervalId
function on(){
intervalId = window.setInterval(function(){
setnum(num=>num+1)
//setnum(num + 1)
//Line 11 results in the number going up once and if i keep pressing the button it goes up by one but flashes between numbers more and more frantically every press. The off button has no effect.
//updateUserMoney()
}, 400);
}
function off(){
clearInterval(intervalId)
}
return (
<>
{num}
<button onClick={()=>on()}>On</button>
<button onClick={()=>off()}>Off</button>
</>
The second issue is that the function I want to run in the interval (that setnum is standing in for) is actually
function updateUserMoney(){
batch(()=>{
dispatch(updateUser({money: user.money + 1, energy: user.energy - 1}))
dispatch(incrementTime(1))
})
}
Here, the incrementTime function works as intended and continues to increment, but the update user function only fires once.
I think it has the same problem that line 11 has where setnum(num + 1) doesn't work but setnum(num => num + 1) does. I haven't used the second syntax much and don't understand why it's different can anybody tell me?
Here's the full code
import { useState } from "react";
import { batch, useDispatch, useSelector } from "react-redux";
import { incrementTime, updateUser } from "../actions";
const GeneralActions = () => {
const dispatch = useDispatch()
const user = useSelector((state)=>state.user)
const [num, setnum]= useState(0)
let intervalId
function updateUserMoney(){
batch(()=>{
dispatch(updateUser({money: user.money + 1, energy: user.energy - 1}))
dispatch(incrementTime(1))
})
}
function on(){
intervalId = window.setInterval(function(){
updateuserMoney()
setnum(num=>num+1)
}, 400);
}
function off(){
clearInterval(intervalId)
}
return (
<>
<br/>
<>{num}</>
<button onClick={()=>on()}>On</button>
<button onClick={()=>off()}>Off</button>
</>
);
}
export default GeneralActions;
Any insight is appreciated. Thank you!
Every time you set a new state value in React, your component will rerender. When your GeneralActions component rerenders, your entire function runs again:
const GeneralActions = () => {
// code in here runs each render
}
This means things such as intervalId, will be set to undefined when it runs let intervalId; again, and so on this particular render you lose the reference for whatever you may have set it to in the previous render. As a result, when you call off(), it won't be able to refer to the intervalId that you set in your previous render, and so the interval won't be cleared. If you want persistent variables (that aren't related to state), you can use the useRef() hook like so:
const GeneralActions = () => {
const intervalIdRef = useRef();
...
function on(){
clearInterval(intervalIdRef.current); // clear any currently running intervals
intervalIdRef.current = setInterval(function(){
...
}, 400);
}
function off(){
clearInterval(intervalIdRef.current);
}
}
One thing that I've added above is to clear any already created intervals when on() is executed, that way you won't queue multiple. You should also call off() when your component unmounts so that you don't try and update your state when the component no longer exists. This can be done using useEffect() with an empty dependency array:
useEffect(() => {
return () => off();
}, []);
That should sort out the issue relating to being unable to clear your timer.
Your other issue is with regards to setNum() is that you have a closure over the num variable for the setTimeout() callback. As mentioned above, every time your component re-renders, your function body is executed, so all of the variables/functions are declared again, in essence creating different "versions" of the num state each render. The problem you're facing is that when you call setInterval(function(){}), the num variable within function() {} will refer to the num version at the time the function was created/when setInterval() function was called. This means that as you update the num state, your component re-renders, and creates a new num version with the updated value, but the function that you've passed to setInterval() still refers to the old num. So setNum(num + 1) will always add 1 to the old number value. However, when you use useState(num => num + 1), the num variable that you're referring to isn't the num "version"/variable from the surrounding scope of the function you defined, but rather, it is the most up to date version of the num state, which allows you to update num correctly.
Attempted to translate an example code from class to functional component and faced the problem.
the target file is in components/Wheel/index.js
Key function that causes problem
const selectItem = () => {
if (selectedItem === null) {
const selectedItem = Math.floor(Math.random() * items.length);
console.log(selectedItem);
setSelectedItem(selectedItem);
} else {
setSelectedItem(null);
let t= setTimeout(() => {
selectItem()
}, 500);
clearTimeout(t);
}
};
First time is normal,
from second time onward,
2 clicks are needed for the wheel to spin.
I had to add clearTimeout() or infinite loop is resulted, but the same does not happen in the original.
Original working example in class
My version in functional component.
MyVersion
Thank you.
What an excellent nuance of hooks you've discovered. When you call selectItem in the timeout, the value of selectedItem that is captured in lexical scope is the last value (not null).
There's two answers, a simple answer and a better working answer.
The simple answer is you can accomplish it be simply separating the functions: https://codesandbox.io/s/spinning-wheel-game-forked-cecpi
It looks like this:
const doSelect = () => {
setSelectedItem(Math.floor(Math.random() * items.length));
};
const selectItem = () => {
if (selectedItem === null) {
doSelect();
} else {
setSelectedItem(null);
setTimeout(doSelect, 500);
}
};
Now, read on if you dare.
The complicated answer fixes the solution for the problem if items.length may change in between the time a timer is set up and it is fired:
https://codesandbox.io/s/spinning-wheel-game-forked-wmeku
Rerendering (i.e. setting state) in a timeout causes complexity - if the component re-rendered in between the timeout, then your callback could've captured "stale" props/state. So there's a lot going on here. I'll try and describe it as best I can:
const [selectedItem, setSelectedItem] = useState(null);
// we're going to use a ref to store our timer
const timer = useRef();
const { items } = props;
// this is just the callback that performs a random select
// you can see it is dependent on items.length from props
const doSelect = useCallback(() => {
setSelectedItem(Math.floor(Math.random() * items.length));
}, [items.length]);
// this is the callback to setup a timeout that we do
// after the user has clicked a "second" time.
// it is dependent on doSelect
const doTimeout = useCallback(() => {
timer.current = setTimeout(() => {
doSelect();
timer.current = null;
}, 500);
}, [doSelect]);
// Here's the tricky thing: if items.length changes in between
// the time we rerender and our timer fires, then the timer callback will have
// captured a stale value for items.length.
// The way we fix this is by using this effect.
// If items.length changes and there is a timer in progress we need to:
// 1. clear it
// 2. run it again
//
// In a perfect world we'd be capturing the amount of time remaining in the
// timer and fire it exactly (which requires another ref)
// feel free to try and implement that!
useEffect(() => {
if (!timer.current) return;
clearTimeout(timer.current);
doTimeout();
// it's safe to ignore this warning because
// we know exactly what the dependencies are here
}, [items.length, doTimeout]);
const selectItem = () => {
if (selectedItem === null) {
doSelect();
} else {
setSelectedItem(null);
doTimeout();
}
};
There is a component "DateForm" that changes the global state of "counterInfo" when the form is submitted.
//DateForm component submittal function.
const submitDate = () =>{
props.setCounterInfo(dateInfo); //passes date info to be counterInfo state in App.js
props.setShowInputForm(false); //DateInfo component is no longer rendered
}
then, in app.js the counterInfo state is passed to the Timer component
const App = () => {
const [showInputForm, setShowInputForm] = useState(false);
const [counterInfo, setCounterInfo] = useState(undefined);
return (
<>
<Timer
counterInfo = {counterInfo}
></Timer>
{showInputForm &&
<DateForm
setShowInputForm = {setShowInputForm}
setCounterInfo = {setCounterInfo}
></DateForm>}
</>
);
}
There is a useEffect hook inside of the Timer function that, on a one second interval, used the value of counterInfo.
//Inside the Timer Component
const [currTime, setCurrTime] = useState(null);
useEffect (() => {
setInterval(() => {
let timeLeft = (new Date(`${Months(props.counterInfo.year)[props.counterInfo.month-1].name} ${props.counterInfo.day} ${props.counterInfo.year} ${props.counterInfo.hour}:${props.counterInfo.minute}:${props.counterInfo.second}`).getTime()) - new Date().getTime();
setCurrTime(timeLeft);
},1000);
return(clearInterval());
}, [props, setCurrTime]);
What I intended to happen is for the value of timeLeft in Timer.js to update when the value of counterInfo is updated in DateForm, however, when the value is changed in DateForm, the result of both the new value of counterInfo and the old one both flash when the value of timeLeft is used in Timer.js. This issue isn't caused by any code in Timer.js becuase I tried moving the useEffect hook to app.js and passing the value down to Timer but the problem persisted. The only place that the setCounterInfo state is changed is in the DateForm component.
Does anyone have any idea how to fix this?
First, you have bit mis-syntax at interval decleration
useEffect (() => {
let interval = setInterval(() => {...},1000);
return () => clearInterval(interval);
}, [props, setCurrTime]);
But unrelated, React by default re-applies effects after every render. This is intentional and helps avoid a whole class of bugs that are present in React components.
When it comes to intervals, its specifical matters cause if a render was applied every time setInterval is called, it never will get a chance to actually run
In other words, this code might have some side effects as useEffect in each run cares only of the existing values in that time and forget everything else, and interval isn't like so.
For that from my point of view the best practice is to create useInterval custom hook, that inside will store the callback for the meanwhile
function useInterval(callback) {
const savedCallback = React.useRef();
useEffect(() => {
savedCallback.current = callback;
});
useEffect(() => {
function run() {
savedCallback.current();
}
let interval = setInterval(run ,1000);
return () => clearInterval(interval);
}, [])
}
//Inside the Timer Component
const [currTime, setCurrTime] = useState(null);
useInterval(()=>
setCurrTime((new Date(`${Months(props.counterInfo.year)[props.counterInfo.month-1].name} ${props.counterInfo.day} ${props.counterInfo.year} ${props.counterInfo.hour}:${props.counterInfo.minute}:${props.counterInfo.second}`).getTime()) - new Date().getTime()))