Reputation: 111
So, i'm learning React, really new here. The short question is really in the title, WHY? ⬇
In a exercise to make a countdown, i tried to make the function myself before see the full video, where it uses useEffect
This was my code:
...
function countdown(){
setInterval(()=>{
setTime(time-1) // an useState var
}, 1000)
}
...
<button onclick={countdown}
Relaying on React to "react" to the variable changing values, to render it out.
Pretty straightforward, as in this working vanillaJS script: (caution to infinity "loop" on runnin it)
let secs = 10;
const counter = document.querySelector('#counter');
counter.innerText = secs;
function countdown() {
setInterval(() => {
counter.innerText = --secs;
}, 1000)
}
<button onclick = countdown() >countdown</button>
<p><span id='counter'>#</span>sec</p>
The unespected thing here is, that in the React version, it goes nuts:
Making click countdown directly, does behave nicely each interaction.
So suppose it has something to do with asynchronous, somehow the Interval not letting the useState update the value, and maybe some kind of enclosing the Interval in a context where each sets a different value to the same var name.
Finally the question: Why the [fork]?? Sure useEffect is way better here, but it really look like React 'seling' a solution to a problem caused by itself. I seein so many "bureaucratic code" in React compared to JS, that makes me feel like going away of the some High-level/abstraction to a low-level more C/Java's like. And that's a really good example.
So i'm genuinely intrigued, what's the good reason that simple counter in React become so harder?
Upvotes: 0
Views: 1449
Reputation: 18249
The reason that, in your first snippet, the timer only ever goes down by 1, is nothing to do with React, but is due to fundamental features of Javascript itself. In your code, the function passed to setInterval
is
()=>{
setTime(time-1) // an useState var
}
time
here is, in your context, a piece of React state - but that really doesn't matter. It's a variable, holding a value - which in your case is the number 10. Actually this creates what is called a "closure" over the outer scope where time
is declared - which in a React function component will be that component function itself. It is possible for such a "closed over" variable to change value between function calls - but this won't happen in a React component, because React relies on immutability. If you actually wrote it like this:
()=>{
time--;
console.log(time);
setTime(time);
}
then you would see in the console that the decrease was happening. But I'm not suggesting you actually do that, because the component would surely break in some way - state variables, just like props, should never be altered or mutated within the component, otherwise React might not realise that it needs to rerender the component.
So instead, React gives you a function that takes care of updating the state - this.setState
in a class component, or the function returned by useState
in a function (this is your setTime
). Whether the component is a function or a class doesn't make any difference, in both cases the function tells React that the state should change, and therefore schedules another render of your component, with the new state. In your example using Hooks, that means the function (= component) is called again, but the relevant useState
call will return the updated value for the time
variable. (You can just shrug this off as "React magic" if you want, but since I'm talking about closures anyway, I should point out that the "magic" is really closures again)
In short then, the problem with the non-working example is that time
never actually updates from the value it had on that particular, first, render. This is something that works quite differently between function and class components, but is in general an advantage of functions.
Note that you could "fix" the code here, and avoid your "stale closure" problem, by using the function argument form:
()=>{
setTime(time => time-1)
}
But that would still lead to "crazy", unpredictable results if you clicked the button more than once. And you also would cause memory leaks because the function would still be called every second (once for each time the user clicked!) even when the component is unmounted.
And that's exactly what useEffect
is for - allowing some "effectful function" (here the timer decrease) to happen on each render (provided something relevant has changed), and also allow you to clean it up. If you just wanted timer to countdown every second, independent of any user actions, you would just do this:
useEffect(() => {
const interval = setInterval(() => setTime(time => time - 1), 1000);
return () => clearInterval(interval);
}, []);
In your actual example, where the countdown should only start when a button is clicked, and you presumably don't want it to start counting down twice as fast on a second click, you could have a Boolean state variable that indicates whether the timer should be running, and check that inside the setInterval
callback before calling setTime
. Then your onclick
would just switch that variable on. Doing it that way should give you the behaviour you want.
Is this quite a bit of boilerplate compared to doing the same in vanilla JS? Yes, it is. If you have a small scale project then it may not make sense to use something like React. It's a matter of judgement, and personal opinion, for each project you build. But there are considerable advantages to being able to write declarative components rather than imperative code as you would with vanilla (or jQuery), especially in large scale applications. Of course React isn't the only declarative framework out there - every developer, and every team, has to make their own decision as to what to use.
Upvotes: 2
Reputation: 4322
To answer your question on the 'why' I highly recommend reading this article by Dan Abramov https://overreacted.io/making-setinterval-declarative-with-react-hooks/
It gets into the exact nitty-gritty of what you are asking.
As for your unexpected behaviour, your countdown sets state, which rerenders the page, which restarts the value of secs
to 10
and then your interval runs again, drops the value to 9
causes a state change, which rerenders the page, and you're stuck in a crazy cycle.
I'm not sure why the clicking changes things, my only guess is that clicking might delay the rerender, and you actually see some of the changes.
But the answer to 'WHY' and the solution to your problem can all be found in that great article.
Upvotes: 1