Jonatas Amaral
Jonatas Amaral

Reputation: 111

WHY doesn't setInterval works in React without 'useEffect'?

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

Answers (2)

Robin Zigmond
Robin Zigmond

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 useEffectis 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

Brett East
Brett East

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

Related Questions