useState hook - passing in callback vs state value - reactjs

In this very simple click counter example:
const App = () => {
const [count, setCount] = useState(0)
const handleClick = () => {
setCount(count + 1)
}
return <button onClick={handleClick}>{count}</button>
}
My understanding of the flow is:
Component gets mounted with count=0
When button is clicked, new count state is set with increment of 1.
That triggers a re-render, so now I have count=1, and repeats.
However, using the same understanding, why does it not work here?
const App = () => {
const [count, setCount] = useState(0)
useEffect(() => {
const timer = setInterval(() => {
setCount(count + 1)
}, 500)
return () => {
clearInterval(timer)
}
}, [])
console.log(count) // this stops at 1, so the timer stops triggering??
return <h1>{count}</h1> // get stuck at 1
}
The outcome of the above code is that the count gets stuck at value 1. (and weirdly the console.log stops too).
I thought each time the setInterval timer triggers, the count increment will cause a re-render with new count value and therefore it will just increase by 1 forever?
The fix here is to simply pass in a function argument to access prevState:
const timer = setInterval(() => {
setCount(oldCount => oldCount + 1)
}, 500)
But why couldn't the first approach work?
Hope someone can point me to a good article or documentation of this, I tried searching around but couldn't get any explanation.

The first approach doesn't work because with this
setCount(count + 1)
You are creating a copy of the count value at that particular time you created the callback. This means that every 500ms you will re-execute this line
setCount(0 + 1)
That won't cause a re-render because react is intelligent enough to understand that you are passing the same value to the setCount function so the re-render would not be necessary.
However by passing a callback to setCount:
setCount(oldCount => oldCount + 1)
You are saying that you want the current value of the state count, so each time the argument of that function will be different and therefore it will cause a re-render.
You can find a doc about this topic here: https://reactjs.org/docs/hooks-reference.html#functional-updates

Related

Infinity loop using useState with useEffect in React

I want to increase the state [second] = [second + 1] after every second.
const [second,setSecond] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setSecond(second+1)
}, 1000);
return () => clearInterval(interval);
}, []);
But it seems that an infinity loop occurs, the [second] just increases once, from 0 to 1, and it stops running.
I changed my code from
setSecond(second+1)
to
setSecond((second) => {return second+1})
And this one runs without problem:
const [second,setSecond] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setSecond((second) => {return second+1})
}, 1000);
return () => clearInterval(interval);
}, []);
Seriously, I still don't get it clearly. Can anybody explain to me why? Thanks in advance!
Looking at your code, I think the issue is with the value of second being bound to 0.
const [second,setSecond] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setSecond(second+1)
}, 1000);
return () => clearInterval(interval);
}, []);
Here when your component mounts, the useEffect is executed. At that time you are creating a function and passing it to setInterval. This function will use the value of second at the time of creation (which equals 0). So everytime when the setInterval runs, it is executing setSecond(0+1) which always equals 1.
The correct way that you have mentioned works because you're giving it a function which gets passed into it the current value of state everytime it is executed.
Ciao, the 2 ways you are using to set second are different.
This way:
setSecond(second+1)
does not consider the previuos value of second. It just try to increment second by 1 by reading current value of second. Considering that setSecond is async, on next setInterval is not guaranteed that second will be updated by the previous setSecond. So in this way you could have glitch.
This way:
setSecond((second) => {return second+1})
is the correct one. Here you are considering the previuos value of second (by using arrow function). So in this case, second will be correctly update.
You could make a test: Take a button and on onclick function try to write:
setSecond(second+1)
setSecond(second+1)
you will see that second will be incremented by 1 (and not by 2 as expected).
Now modify your code like this:
setSecond((second) => {return second+1})
setSecond((second) => {return second+1})
you will see that second will be incremented by 2!
This happends because Hooks are async.

Functional component is rerendered more than should

In the React docs I have found this component that is supposed to be rendered only twice:
function Users() {
const [count, setCount] = useState(0);
console.log("---: " + count)
useEffect(() => {
console.log("useEffect")
const id = setInterval(() => {
console.log("setCount: " + (count + 1))
setCount(count + 1); // This effect depends on the `count` state
}, 1000);
return () => clearInterval(id);
}, []); // đź”´ Bug: `count` is not specified as a dependency
return <h1>{count}</h1>;
}
Try running previous component and you will get this logs where it is visible that a component is rerendered three times. So one unnecessary render happens (only one but still), even though setState has returned the same value. Can someone explain this, please?

How to correctly use React `useCallback`'s dependencies list?

I have an example like this:
codesandebox
I want to modify a state value in a callback, then use the new state value to modify another state.
export default function App() {
const [count, setCount] = useState(0);
const [text, setText] = useState("0");
const [added, setAdded] = useState(false);
const aNotWorkingHandler = useCallback(
e => {
console.log("clicked");
setCount(a => ++a);
setText(count.toString());
},
[count, setCount, setText]
);
const btnRef = useRef(null);
useEffect(() => {
if (!added && btnRef.current) {
btnRef.current.addEventListener("click", aNotWorkingHandler);
setAdded(true);
}
}, [added, aNotWorkingHandler]);
return <button ref={btnRef}> + 1 </button>
However, after this handler got called, count has been successfully increased, but text hasn't.
Can you guys help me to understand why this happened? and how to avoid it cleanly?
Thank you!
If count and state are always supposed to be in lockstep, just with one being a number and one being a string, then i think it's a mistake to have two state variables. Instead, just have one, and derive the other value from it:
const [count, setCount] = useState(0);
const text = "" + count;
const [added, setAdded] = useState(false);
const aNotWorkingHandler = useCallback(
e => {
setCount(a => ++a);
},
[]
);
In the above useCallback, i have an empty dependency array. This is because the only thing that's being used in the callback is setCount. React guarantees that state setters have stable references, so it's impossible for setCount to change, and thus no need to list it as a dependency.
There are few things causing the issue.
Setter does not update the count value immediately. Instead it "schedules" the component to re-render with the new count value returned from the useState hook. When the setText setter is called, the count is not updated yet, because the component didn't have chance to re-render in the mean time. It will happen some time after the handler is finished.
setCount(a => ++a); // <-- this updates the count after re-render
setText(count.toString()); // <-- count is not incremented here yet
You are calling addEventListener only once and it remembers the first value of count. It is good you have aNotWorkingHandler in the dependencies - the onEffect is being re-run when new count and thus new handler function comes. But your added flag prevents the addEventListener from being called then. The button stores only the first version of the handler function. The one with count === 0 closured in it.
useEffect(() => {
if (!added && btnRef.current) { // <-- this prevents the event handler from being updated
btnRef.current.addEventListener("click", aNotWorkingHandler); // <-- this is called only once with the first instance of aNotWorkingHandler
setAdded(true);
} else {
console.log("New event handler arrived, but we ignored it.");
}
}, [added, aNotWorkingHandler]); // <-- this correctly causes the effect to re-run when the callback changes
Just removing the added flag would, of course, cause all the handlers to pile up. Instead, just use onClick which correctly adds and removes the event handler for you.
<button onClick={aNotWorkingHandler} />
In order to update a value based on another value, I'd probably use something like this (but it smells of infinite loop to me):
useEffect(
() => {
setText(count.toString());
},
[count]
);
Or compute the value first, then update the states:
const aNotWorkingHandler = useCallback(
(e) => {
const newCount = count + 1;
setCount(newCount);
setText(newCount.toString());
},
[count]
);
I agree with #nicholas-tower, that if the other value does not need to be explicitly set and is always computed from the first one, it should be just computed as the component re-renders. I think his answer is correct, hope this context answers it for other people getting here.

Handling out of date state in functional components

I am running into issues setting a state created with the 'useState' hook from within async functions.
I've created a codepen to demonstrate: https://codepen.io/james-ohalloran/pen/ZdNwWQ
const Counter = () => {
const [count, setCount] = useState(0);
const increase = () => {
setTimeout(() => {
setCount(count + 1);
},1000);
}
const decrease = () => {
setTimeout(() => {
setCount(count - 1);
},1000)
};
return (
<div className="wrapper">
<button onClick={decrease}>-</button>
<span className="count">{count}</span>
<button onClick={increase}>+</button>
</div>
);
};
In the above example, if you click 'increase' followed by 'decrease'..you will end up with -1 (I would expect it to be 0).
If this was a React class instead of a functional component, I would assume the solution would be to use bind(this) on the function, but I didn't expect this to be an issue with arrow functions.
It is because of using setTimeout
Let's assume that you've called the increase() 10 times in a second.
count will be always 0. Because the state is updated after a second, every increment() called in a second will have an unupdated count.
So every increment() will call setCount(0 + 1);.
So no matter how many times you call in a second, the count is always 1.
Ah, I found a solution. I didn't realize I'm able to reference the previousState from the useState setter function: https://reactjs.org/docs/hooks-reference.html#functional-updates

react hooks and setInterval

Is there any alternative to just keeping a "clock" in the background to implement auto-next (after a few seconds) in carousel using react hooks?
The custom react hook below implements a state for a carousel that supports manual (next, prev, reset) and automatic (start, stop) methods for changing the carousel's current (active) index.
const useCarousel = (items = []) => {
const [current, setCurrent] = useState(
items && items.length > 0 ? 0 : undefined
);
const [auto, setAuto] = useState(false);
const next = () => setCurrent((current + 1) % items.length);
const prev = () => setCurrent(current ? current - 1 : items.length - 1);
const reset = () => setCurrent(0);
const start = _ => setAuto(true);
const stop = _ => setAuto(false);
useEffect(() => {
const interval = setInterval(_ => {
if (auto) {
next();
} else {
// do nothing
}
}, 3000);
return _ => clearInterval(interval);
});
return {
current,
next,
prev,
reset,
start,
stop
};
};
There are differences between setInterval and setTimeout that you may not want to lose by always restarting your timer when the component re-renders. This fiddle shows the difference in drift between the two when other code is also running. (On older browsers/machines—like from when I originally answered this question—you don't even need to simulate a large calculation to see a significant drift begin to occur after only a few seconds.)
Referring now to your answer, Marco, the use of setInterval is totally lost because effects without conditions dispose and re-run every time the component re-renders. So in your first example, the use of the current dependency causes that effect to dispose and re-run every time the current changes (every time the interval runs). The second one does the same thing, but actually every time any state changes (causing a re-render), which could lead to some unexpected behavior. The only reason that one works is because next() causes a state change.
Considering the fact that you are probably not concerned with exact timing, is is cleanest to use setTimeout in a simple fashion, using the current and auto vars as dependencies. So to re-state part of your answer, do this:
useEffect(
() => {
if (!auto) return;
const interval = setTimeout(_ => {
next();
}, autoInterval);
return _ => clearTimeout(interval);
},
[auto, current]
);
Generically, for those just reading this answer and want a way to do a simple timer, here is a version that doesn't take into account the OP's original code, nor their need for a way to start and stop the timer independently:
const [counter, setCounter] = useState(0);
useEffect(
() => {
const id= setTimeout(() => {
setCounter(counter + 1);
// You could also do `setCounter((count) => count + 1)` instead.
// If you did that, then you wouldn't need the dependency
// array argument to this `useEffect` call.
}, 1000);
return () => {
clearTimeout(id);
};
},
[counter],
);
However, you may be wondering how to use a more exact interval, given the fact that setTimeout can drift more than setInterval. Here is one method, again, generic without using the OP's code:
// Using refs:
const [counter, setCounter] = useState(30);
const r = useRef(null);
r.current = { counter, setCounter };
useEffect(
() => {
const id = setInterval(() => {
r.current.setCounter(r.current.counter + 1);
}, 1000);
return () => {
clearInterval(id);
};
},
[] // empty dependency array
);
// Using the function version of `setCounter` is cleaner:
const [counter, setCounter] = useState(30);
useEffect(
() => {
const id = setInterval(() => {
setCounter((count) => count + 1);
}, 1000);
return () => {
clearInterval(id);
};
},
[] // empty dependency array
);
Here is what is going on above:
(first example, using refs): To get setInterval's callback to always refer to the currently acceptable version of setCounter we need some mutable state. React gives us this with useRef. The useRef function will return an object that has a current property. We can then set that property (which will happen every time the component re-renders) to the current versions of counter and setCounter.
(second example, using functional setCounter): Same idea as the first, except that when we use the function version of setCounter, we will have access to the current version of the count as the first argument to the function. No need to use a ref to keep things up to date.
(both examples, continued): Then, to keep the interval from being disposed of on each render, we add an empty dependency array as the second argument to useEffect. The interval will still be cleared when the component is unmounted.
Note: I used to like using ["once"] as my dependency array to indicate that I am forcing this effect to be set up only once. It was nice for readability at the time, but I no longer use it for two reasons. First, hooks are more widely understood these days and we have seen the empty array all over the place. Second, it clashes with the very popular "rule of hooks" linter which is quite strict about what goes in the dependency array.
So applying what we know to the OP's original question, you could use setInterval for a less-likely-to-drift slideshow like this:
// ... OP's implementation code including `autoInterval`,
// `auto`, and `next` goes above here ...
const r = useRef(null);
r.current = { next };
useEffect(
() => {
if (!auto) return;
const id = setInterval(() => {
r.current.next();
}, autoInterval);
return () => {
clearInterval(id);
};
},
[auto]
);
Because the current value is going to change on every "interval" as long as it should be running, then your code will start and stop a new timer on every render. You can see this in action here:
https://codesandbox.io/s/03xkkyj19w
You can change setInterval to be setTimeout and you will get the exact same behaviour. setTimeout is not a persistent clock, but it doesn't matter since they both get cleaned up anyways.
If you do not want to start any timer at all, then put the condition before setInterval not inside of it.
useEffect(
() => {
let id;
if (run) {
id = setInterval(() => {
setValue(value + 1)
}, 1000);
}
return () => {
if (id) {
alert(id) // notice this runs on every render and is different every time
clearInterval(id);
}
};
}
);
So far, it seems that both solutions below work as desired:
Conditionally creating timer — it requires that useEffect is dependent both on auto and current to work
useEffect(
() => {
if (!auto) return;
const interval = setInterval(_ => {
next();
}, autoInterval);
return _ => clearInterval(interval);
},
[auto, current]
);
Conditionally executing update to state — it does not require useEffect dependencies
useEffect(() => {
const interval = setInterval(_ => {
if (auto) {
next();
} else {
// do nothing
}
}, autoInterval);
return _ => clearInterval(interval);
});
Both solutions work if we replace setInterval by setTimeout
You could use useTimeout hook that returns true after specified number of milliseconds.
https://github.com/streamich/react-use/blob/master/docs/useTimeout.md

Resources