React: When does not the count value changes in setInterval? - javascript

I know this code will work and the count value will be updated after every 2 seconds:
import React from 'react';
function App() {
const [count, setCount] = React.useState(0);
React.useEffect(() => {
setInterval(() => {
setCount(prev => prev + 1);
}, 2000);
}, []);
return <h1>{count}</h1>;
}
I am trying to understand why the below code doesn't work:
React.useEffect(() => {
const interval = setInterval(() => {
setCount(count + 1);
console.log('hello');
}, 2000);
return () => clearInterval(interval);
}, []);
In this case, count stays at 1 but the setInterval is called after every 2 seconds because hello is being logged every 2 seconds.
How setInterval is being called when component is not being rendered? Does it operate outside of React lifecycle or something?
When I pass callback to setCount(prev => prev + 1), then it works fine? Why? How the prev in callback getting the updated value?
I have read similar answers but still it is not making sense to me so if someone could please clear the doubt.
Thanks

How setInterval is being called when component is not being rendered? Does it operate outside of React lifecycle or something?
Yes, setInterval is a function that's part of the core javascript language. By calling setInterval, you ask the browser to call the specified function every 2000ms. React is not involved in this.
When I pass callback to setCount(prev => prev + 1), then it works fine? Why? How the prev in callback getting the updated value?
The useState hook is designed to allow this interaction. React promises that if you pass a function into setCount, then react will call your function and pass in the latest value.
The exact code with which they implement this can be tricky to step through since they reuse a lot of general purpose functions to implement several different hooks, but if you want to poke around their codebase you can look here: https://github.com/facebook/react/blob/main/packages/react-reconciler/src/ReactFiberHooks.new.js#L1525

When you create the interval function, count will be the value at the moment of creation (so 0).
When it gets called later, it will still hold that value, since it won't get changed in that context. So it will always set it to 0 + 1, so 1.
When you pass a callback function, it will use the current, correct value.
Hope this could clear things up!

This version: setCount(prev => prev + 1) uses the previous state value every-time the interval gets run.
This version: setCount(count + 1) uses count when the useEffect was las run (in this case 0, so 0+1). Because there is an empty dependency array this hook is only run once.
If count was included in the dependency array of useEffect then it would get re-run when the value of count changes.
Running this may help to see what's going on:
React.useEffect(() => {
console.log('running useEffect')
const interval = setInterval(() => {
setCount(count + 1);
console.log('hello ', count);
}, 500);
return () => {
console.log('clear interval')
clearInterval(interval);
}
}, [count]);

Related

SolidJS: Using createEffect to observe signal changes

I have started a new journey with SolidJS after being in the Angularverse since Angular 2 :)
I've been following the Effects tutorial on their official website but I'm not able to make it work the way I want.
I thought createEffect would keep track and run every time the count signal changed, or so I understood from here:
The effect automatically subscribes to any signal that is read during
the function's execution and reruns when any of them change.
Unfortunately, clicking the button doesn't seem to have any effect. So, what am I missing?
import { render } from "solid-js/web";
import { createSignal, createEffect } from "solid-js";
function Counter() {
const [count, setCount] = createSignal(0);
const doubleCount = () => setCount(count() * 2);
createEffect(() => {
console.log("The count is now", count());
});
return <button onClick={doubleCount}>Click Me</button>;
}
render(() => <Counter />, document.getElementById('app'));
It is a feature !
What is happening is that 0 * 2 = 0 so it doesn't trigger any effect because the value of count didn't change.
Try
const [count, setCount] = createSignal(1);
to see a console.log each time the value change with your click
Your code is correct and runs as expected. Two possible reason why you don't see change:
If tere is no log, it means you don't have #app element on the page.
If there is only one log but no subsequent ones, that is because 0*2 is 0, value does not change, so it does not trigger re-run.
https://playground.solidjs.com/anonymous/7ec9c592-6c30-43a0-97a7-5f26917b4334

React useState hook, calling setState with function

There is a concept in React (when using hooks) that confuses me.
I made a component for explanation (that increases a counter):
const [counter, setCounter] = useState(0); // counter hook
// code will follow
// render
return (
<div>
<button onClick={handleClick}>+</button>
<h3>{counter}</h3>
</div>
);
For the handler function, I have seen different options to set the state.
First method (using setState() normally):
const handleClick = () => {
setCounter(counter + 1);
};
Second method (creating a function inside setState() and returning the new value):
const handleClick = () => {
setCounter((counter) => {
return counter + 1;
});
};
I thought the difference would be that with the second method, you could immediately do a callback after setting the state, like this:
const handleClick = () => {
setCounter((counter) => {
return counter + 1;
}, () => {
console.log(counter); // trying callback after state is set
});
};
But when trying this (with both methods), the console displays the following error message:
Warning: State updates from the useState() and useReducer() Hooks don't support the second callback argument. To execute a side effect after rendering, declare it in the component body with useEffect().
So I think in both cases, using useEffect() as a callback on setState() is the right way.
My question is: what is the difference between the two methods and what method is best to set the state. I have read about state immutability but can not immediately see how it would make a difference in this example.
In your case it's the same.
Basically when your state is computed with your previous state you can use the second approach which gets the previous value.
Have a look in React docs about this:
Functional updates
Since this question is gaining some attention I will add this example.
<button onClick={() => setCount(0)}>Reset</button>
<button onClick={() => setCount((prevCount) => prevCount - 1)}>-</button>
<button onClick={() => setCount((prevCount) => prevCount + 1)}>+</button>
You can see that for + and - the functional setStateis being used, this is because the new state value is computed using the previous state (you are adding/subtracting from the previous count value).
The reset button is using the normal form, because it doesn't compute the new state value based on a computation on the old value, it always just sets it to a fixed number (for example 0).
So in my case, the best thing to do would have been using the functional setState.

In ReactJS, if we don't supply a dependency array for useEffect, it can cause the function to be called every time?

I saw in the ReactJS docs that the dependency array in useEffect(fn, []) is optional, and not supplying it should be the same as supplying an empty array.
However, if I have the code as on: https://codesandbox.io/s/trusting-sunset-h6ip9?file=/src/Count.js
useEffect(() => {
console.log("SETTING INTERVAL");
setInterval(() => {
console.log("NOW", Date.now() / 1000);
setDuration(Date.now() - startTime);
}, 1000);
}, []);
With the [] above, everything runs as expected. However, if I remove it and run again, as can be seen on: https://codesandbox.io/s/dreamy-platform-l93bv?file=/src/Count.js
We can see in the Developer's console or Codesandbox.io's console that the line "SETTING INTERVAL" is continuously being printed.
Supposedly, I thought having the empty array and not having in should make no difference, but in this case we need to put it in? What is the reason?
Very simply put, the array you provide is telling useEffect that it should run when the values inside the array changes. Since the value is static it will only run once. If you remove the array it will run on every render. You can also put a variable inside the array to tell useEffect to update whenever the variable changes.
There is a major and quite obvious difference between an empty array and an 'no deps'.
When you use an empty array, you are actually using componentDidMount in class components. But if you do not leave the same empty array, you will see that your counter does not work well.
If you do not want to use an empty array, you must clean up your useEffect.
See docs about "Effects Without Cleanup"
The reason the function does not work properly is that every time your component is updated, the counter also runs very fast. But with a simple clean up command this problem can be solved.
See docs about "Effects with Cleanup"
You can see an example for your hooks bellow:
useEffect(() => {
console.log("SETTING INTERVAL");
setInterval(() => {
console.log("NOW", Date.now() / 1000);
setDuration(Date.now() - startTime);
return function cleanup() {
return 0;
// Also you can use 'clearInterval' here or anything else to stop rendering more
};
}, 1000);
});
you need use like this:
useEffect(() => {
const interval = setInterval(() => {
console.log("NOW", Date.now() / 1000);
setDuration(Date.now() - startTime);
}, 1000);
return ()=> clearInterval(interval)
}, []);

React component reacts unexpectedly

The following is a simple react component:
import React from "react";
import { useState } from "react";
export default function Comp() {
let [count, setCount] = useState(1);
function countUp(){
setCount(count + 1);
}
setInterval(countUp, 1000);
return <h2>{count}</h2>
}
I expected the counter to go up every second
But for some reason, after ten - twenty seconds something starts to go wrong
See here:
https://stackblitz.com/edit/react-az7qgn?file=src/comp.jsx
Can anyone explain this?
You should use useEffect hook to set up that properly. I can provide an example.
import React, { useState, useEffect } from "react";
export default function Comp() {
const [count, setCount] = useState(1);
useEffect(() => {
const interval = setInterval(() => {
setCount(state => state + 1);
}, 1000);
return () => clearInterval(interval);
}, []);
return <h2>{count}</h2>
}
A couple of notes.
In general, you would prefer const over let, but this is mandatory when destructuring things coming from React.
I suggest to read Using the Effect Hook on React docs to more information about useEffect.
Basically, useEffect allows you to achieve similar results to componentDidMount and componentDidUpdate lifecycle methods for class components. Also, in this specific case, by returning a function in useEffect callback, we make sure to clear the scheduled callback when it's time to clean up, which means after each run. This actually avoids the mess of stacking many setInterval on top of each other.
Also, when you setCount it's preferable to get the previous state by using the callback form, because that will be always up-to-date.
When calling setInterval(), it returns an interval id. Your code is not saving the variable, and thus you cannot reset it. On smaller iterations, you will not see the changes for every iteration. But, as the number of times that setInterval() is called increases from 0 to N, more timers are being initiated, and you will rapidly see flashes of numbers as they increase, because every interval is changing the state of count.
In other words, you are creating more and more timers as time goes on, rather than creating timers for one-time use. You will need to call clearInterval(timer_id_goes_here) to clear the timer. See code examples in the link below.
https://www.w3schools.com/jsref/met_win_setinterval.asp

Store a callback in useRef()

Here is an example of a mutable ref storing the current callback from the Overreacted blog:
function useInterval(callback, delay) {
const savedCallback = useRef();
// update ref before 2nd effect
useEffect(() => {
savedCallback.current = callback; // save the callback in a mutable ref
});
useEffect(() => {
function tick() {
// can always access the most recent callback value without callback dep
savedCallback.current();
}
let id = setInterval(tick, delay);
return () => clearInterval(id);
}, [delay]);
}
However the React Hook FAQ states that the pattern is not recommended:
Also note that this pattern might cause problems in the concurrent mode. [...]
In either case, we don’t recommend this pattern and only show it here for completeness.
I found this pattern to be very useful in particular for callbacks and don't understand why it gets a red flag in the FAQ. For example, a client component can use useInterval without needing to wrap useCallback around the callback (simpler API).
Also there shouldn't be a problem in concurrent mode, as we update the ref inside useEffect. From my point of view, the FAQ entry might have a wrong point here (or I have misunderstood it).
So, in summary:
Does anything fundamentally speak against storing callbacks inside mutable refs?
Is it safe in concurrent mode when done like it is in the above code, and if not, why not?
Minor disclaimer: I'm not a core react dev and I haven't looked at the react code, so this answer is based on reading the docs (between the lines), experience, and experiment
Also this question has been asked since which explicitly notes the unexpected behaviour of the useInterval() implementation
Does anything fundamentally speak against storing callbacks inside mutable refs?
My reading of the react docs is that this is not recommended but may still be a useful or even necessary solution in some cases hence the "escape hatch" reference, so I think the answer is "no" to this. I think it is not recommended because:
you are taking explicit ownership of managing the lifetime of the closure you are saving. You are on your own when it comes to fixing it when it gets out of date.
this is easy to get wrong in subtle ways, see below.
this pattern is given in the docs as an example of how to work around repeatedly rendering a child component when the handler changes, and as the docs say:
it is preferable to avoid passing callbacks deep down
by e.g. using a context. This way your children are less likely to need re-rendering every time your parent is re-rendered. So in this use-case there is a better way to do it, but that will rely on being able to change the child component.
However, I do think doing this can solve certain problems that are difficult to solve otherwise, and the benefits from having a library function like useInterval() that is tested and field-hardened in your codebase that other devs can use instead of trying to roll their own using setInterval directly (potentially using global variables... which would be even worse) will outweigh the negatives of having used useRef() to implement it. And if there is a bug, or one is introduced by an update to react, there is just one place to fix it.
Also it might be that your callback is safe to call when out of date anyway, because it may just have captured unchanging variables. For example, the setState function returned by useState() is guaranteed not to change, see the last note in this, so as long as your callback is only using variables like that, you are sitting pretty.
Having said that, the implementation of setInterval() that you give does have a flaw, see below, and for my suggested alternative.
Is it safe in concurrent mode, when done like in above code (if not, why)?
Now I don't exactly know how concurrent mode works (and it's not finalized yet AFAIK), but my guess would be that the window condition below may well be exacerbated by concurrent mode, because as I understand it it may separate state updates from renders, increasing the window condition that a callback that is only updated when a useEffect() fires (i.e. on render) will be called when it is out of date.
Example showing that your useInterval may pop when out of date.
In the below example I demonstrate that the setInterval() timer may pop between setState() and the invocation of the useEffect() which sets the updated callback, meaning that the callback is invoked when it is out of date, which, as per above, may be OK, but it may lead to bugs.
In the example I've modified your setInterval() so that it terminates after some occurrences, and I've used another ref to hold the "real" value of num. I use two setInterval()s:
one simply logs the value of num as stored in the ref and in the render function local variable.
the other periodically updates num, at the same time updating the value in numRef and calling setNum() to cause a re-render and update the local variable.
Now, if it were guaranteed that on calling setNum() the useEffect()s for the next render would be immediately called, we would expect the new callback to be installed instantly and so it wouldn't be possible to call the out of date closure. However the output in my browser is something like:
[Log] interval pop 0 0 (main.chunk.js, line 62)
[Log] interval pop 0 1 (main.chunk.js, line 62, x2)
[Log] interval pop 1 1 (main.chunk.js, line 62, x3)
[Log] interval pop 2 2 (main.chunk.js, line 62, x2)
[Log] interval pop 3 3 (main.chunk.js, line 62, x2)
[Log] interval pop 3 4 (main.chunk.js, line 62)
[Log] interval pop 4 4 (main.chunk.js, line 62, x2)
And each time the numbers are different illustrates the callback has been called after the setNum() has been called, but before the new callback has been configured by the first useEffect().
With more trace added the order for the discrepancy logs was revealed to be:
setNum() is called,
render() occurs
"interval pop" log
useEffect() updating ref is called.
I.e. the timer pops unexpectedly between the render() and the useEffect() which updates the timer callback function.
Obviously this is a contrived example, and in real life your component might be much simpler and not actually be able to hit this window, but it's at least good to be aware of it!
import { useEffect, useRef, useState } from 'react';
function useInterval(callback, delay, maxOccurrences) {
const occurrencesRef = useRef(0);
const savedCallback = useRef();
// update ref before 2nd effect
useEffect(() => {
savedCallback.current = callback; // save the callback in a mutable ref
});
useEffect(() => {
function tick() {
// can always access the most recent callback value without callback dep
savedCallback.current();
occurrencesRef.current += 1;
if (occurrencesRef.current >= maxOccurrences) {
console.log(`max occurrences (delay ${delay})`);
clearInterval(id);
}
}
let id = setInterval(tick, delay);
return () => clearInterval(id);
}, [delay]);
}
function App() {
const [num, setNum] = useState(0);
const refNum = useRef(num);
useInterval(() => console.log(`interval pop ${num} ${refNum.current}`), 0, 60);
useInterval(() => setNum((n) => {
refNum.current = n + 1;
return refNum.current;
}), 10, 20);
return (
<div className="App">
<header className="App-header">
<h1>Num: </h1>
</header>
</div>
);
}
export default App;
Alternative useInterval() that does not have the same problem.
The key thing with react is always to know when your handlers / closures are being called. If you use setInterval() naively with arbitrary functions then you are probably going to have trouble. However, if you ensure your handlers are only called when the useEffect() handlers are called, you will know that they are being called after all state updates have been made and you are in a consistent state. So this implementation does not suffer in the same way as the above one, because it ensures the unsafe handler is called in useEffect(), and only calls a safe handler from setInterval():
import { useEffect, useRef, useState } from 'react';
function useTicker(delay, maxOccurrences) {
const [ticker, setTicker] = useState(0);
useEffect(() => {
const timer = setInterval(() => setTicker((t) => {
if (t + 1 >= maxOccurrences) {
clearInterval(timer);
}
return t + 1;
}), delay);
return () => clearInterval(timer);
}, [delay]);
return ticker;
}
function useInterval(cbk, delay, maxOccurrences) {
const ticker = useTicker(delay, maxOccurrences);
const cbkRef = useRef();
// always want the up to date callback from the caller
useEffect(() => {
cbkRef.current = cbk;
}, [cbk]);
// call the callback whenever the timer pops / the ticker increases.
// This deliberately does not pass `cbk` in the dependencies as
// otherwise the handler would be called on each render as well as
// on the timer pop
useEffect(() => cbkRef.current(), [ticker]);
}

Categories

Resources