Handling out of date state in functional components - reactjs

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

Related

understanding useCallback and setState

I have some simple lines of code to test the useCallback and setState hook. First I try to write like this:
const App = () => {
const [count, setCount] = useState(0)
const increase = useCallback(() => {
setCount(count + 1) // Passing value to change state
}, [])
console.log(count)
return (
<div id="main">
<h1>Count: {count}</h1>
<button onClick={increase}>Increase</button>
</div>
)
}
export default App;
and then, passing the callback to setCount instead like this:
const App = () => {
const [count, setCount] = useState(0)
const increase = useCallback(() => {
setCount(count => count + 1) // Passing callback
}, [])
console.log(count)
return (
<div id="main">
<h1>Count: {count}</h1>
<button onClick={increase}>Increase</button>
</div>
)
}
export default App;
I know it is weird to use useCallback in this situation, just for some testing. The question is that why I pass the callback to setCount, the Increase button work correctly, whereas the first one will just re-render on the first click
In the first case useCallback closes over the count and at that time count was 0 so It always changes value to 1
So to make it working you should add count dependency to useCallback as:
CODESANDBOX LINK
const increase = useCallback(() => {
setCount(count + 1); // Passing value to change state
}, [count]);
In the second case it is not taking the value from the closure instead it is taking the current value and adds one to it and set the value.
ADDITIONAL INFO
You can use react-hooks/exhaustive-deps eslint rule to know which dependency is missing and tell you how to correct it.
useCallback() hasn't been used correctly here: the dependencies array should have count in it. So:
const increase = useCallback(() => {
setCount(count + 1) // Passing value to change state
}, [count])
With an empty array as a dependency, it will be memoized for the lifetime of the component and will not update the current count value.
Source: https://reactjs.org/docs/hooks-reference.html#usecallback
Code sandbox
On a side note, using useCallback() is "overoptimizing" in this case, we are complicating the code while the performance impact to the end users is not noticeable. I would only use it to prevent rerenders in complex views e.g. in a huge list.

useState hook - passing in callback vs state value

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

Why my State values cannot be updated with a timer in react?

Im new in react and Im doing some basic stuff... so, I started using the states and I found it quite useful, however, there is one thing that doesnt make sense in my mind.
It all started by having a time displayed, something like this
function MyAPp() {
const initValue = new Date().toLocaleTimeString();
const [value, setCount] = React.useState(initValue);
function updateValue() {
setCount(new Date().toLocaleTimeString())
}
setInterval(updateValue, 1000)
return <div>
<label> The value of my state is { value } </label>
</div>
So, the above works great, updates the time every second and nothing wrong with it...
Now when I do this:
function MyApp() {
const [count, setCount] = React.useState(123);
function updateValue() {
setCount(count + 1)
}
setInterval(updateValue, 1000)
return <div>
<label> The value of my state is { count } </label>
</div>
}
When it reaches 8 or 9, starts to have a weird behaviour, it starts to get updated less than every second, and the update goes jumping from number to number, like from 8 to 9 and then back to 8, then quickly back to 9 and 10, 11 and back to 9 or 8 again and it just gets crazy....
Checking about the limitations of getting the value and use it to set the count, makes no sense why is failing this way.
And even worse, why the interval seems to be affected, feels like it would be getting into a loop by calling MyApp() or something like that, which is not happening...
Any idea what could be wrong with this?
There are 2 issues in the above-mentioned implementation.
setInterval is being called on every render.
This can be solved by calling this inside useEffect without any dependencies. It'll make sure the interval is being called only once.
This is a case of stale props or state in react hook. For more refer the official documentation.
The setter method of state accepts a callback. It gives the current state in the callback which will always have the final update value.
Kent Dodds mentions this as a Lazy initialization of state. Here's a really good blog on the same.
Here's the working example solving this issue at codesandbox
Code
import { useState, useEffect } from "react";
export default function App() {
const [count, setCount] = useState(123);
useEffect(() => {
setInterval(() => {
setCount((prevCount) => prevCount + 1);
}, 1000);
}, []);
return (
<div>
<label> The value of my state is {count} </label>
</div>
);
}
becuase setInterval is a method that calls a function or runs some code after specific intervals of time, as specified through the second parameter.
clearInterval is a function or block of code that is bound to an interval executes until it is stopped. After the React component unmounts the interval is cleared.
by two methods , your scheduling works good in display count.
export default function App() {
const [count, setCount] = React.useState(123);
useEffect(() => {
const interval = setInterval(() => {
setCount(count + 1);
}, 1000);
return () => clearInterval(interval);
}, [count]);
return (
<div>
<label> The value of my state is {count} </label>
</div>
);
}
You are doing it absolutely wrong, firstly as you can see your setInterval is registered on every render which is causing the unexpected behavior and secondly it's always recommended to merge the old and new state when using setInterval as the value will remain same in the setInterval callback.
Here is the solution to your problem.
function MyApp() {
const [count, setCount] = React.useState(123);
const updateValue = React.useCallback(() => {
setCount((old) => old + 1);
}, []);
React.useEffect(() => {
setInterval(updateValue, 1000);
}, [updateValue]);
return (
<div>
<label> The value of my state is {count} </label>
</div>
);
}
So in this solution we have used React.useEffect which has only one dependency which means when the dependency will be updated the call of React.useEffect will be invoked but in this case it will only be invoked once the component is rendered.
Checkout this demo

Why does a count using React Hooks useState() alternate between two numbers?

The following is a React Hooks experiment of using useState(). It works fine except when the + button was clicked on, then the number can be alternating from 7001 and 7000 and then flashing between some numbers quickly.
Actually, without clicking on the +, the number behaved well but up to about 8000 or 9000, then it might start to flash between some numbers. Why is that and how can it be fixed?
P.S. initial debugging finding was that: it seems Counter() was called multiple times, setting up an Interval Timer every time. So "magically", it seems the useState() ran only once -- for some unknown and magical reason -- or maybe it ran more than once but just returned the exact same content each time, for some magical mechanism. The initial value of 0 really was so for the first time. When it was useState(0) for future times, the count was not 0... we wouldn't want that, but then it wasn't that functional (as in a math function) either.
function Counter() {
const [count, setCount] = React.useState(0);
setInterval(() => {
setCount(count + 1000);
}, 1000);
return (
<div>
<button onClick={() => setCount(count + 1)}> + </button>
{ count }
<button onClick={() => setCount(count - 1)}> - </button>
</div>
);
}
ReactDOM.render(<Counter />, document.querySelector("#root"));
button { margin: 0 1em }
<script src="https://unpkg.com/react#16/umd/react.development.js" crossorigin></script>
<script src="https://unpkg.com/react-dom#16/umd/react-dom.development.js" crossorigin></script>
<div id="root"></div>
Code in functional component gets executed every time when component is re-rendered. So, on each re-render you are starting an infinite timer that adds 1000 to counter each second
Each time you change component's state, React re-renders it. Meaning, every execution of setCount leads to new re-render and new timer is started
Also, setCount is asynchronous and if you need to rely on previous state to determine next one, you should call with callback, it like demonstrated in other answer (setCount(c => c + 1))
Something like this is supposed to work:
import React, {useState, useRef, useEffect} from 'react';
function Counter() {
const [count, setCount] = useState(0);
//useRef gives us an object to store things between re-renders
const timer = useRef();
useEffect(() => {
timer.current = setInterval(() => {
setCount(count => count + 1000);
}, 1000);
//If we return a function, it will be called when component is dismounted
return () => {
clearInterval(timer.current);
}
}, []);
return (
<div>
<button onClick={() => setCount(count => count + 1)}> + </button>
{ count }
<button onClick={() => setCount(count => count - 1)}> - </button>
</div>
);
}
Not quite sure about the 'why is that' but it's fixed with substituting setCount(c => c + 1) in the buttons and setCount(c => c + 1000) in the interval.
Putting the 'setInterval' in an effect also makes sure that there is only one interval...
React.useEffect(() => {
setInterval(() => {
setCount(c => c + 1000);
}, 1000);
},[])
PS Counter() gets called on every render, I think... while useState only gets called once per mounting by design.

With react hooks set state, in what cases I have to pass a function to set state?

here is my code:
function Tiker() {
var [count, setCount] = useState(0);
useEffect(() => {
var timerID = setInterval(_=>
setCount(count=>count+1)//setCount(count+1) wont work
, 1000);
return function cleanup() {
clearInterval(timerID);
};
}, []);
return <div>
this is ticker
<button onClick={() =>
setCount(count + 1)//setCount(count+1) does work
}>up </button>
{count}
</div>
}
By trial and error I discovered that if I use setCount from within setinterval callback, I have to pass a callback to the set state rather than just value.
its not the case if I call from onclick.
Why is that?
The problem is the second argument of useEffect
useEffect(() => {
var timerID = setInterval(_=>
setCount(count=>count+1)//setCount(count+1) wont work
, 1000);
return function cleanup() {
clearInterval(timerID);
};
}, []);
It is empty array ([]). It defines list of dependencies for hook. As it empty, it means that hook is not dependent from any value of state or props. So count variable is consumed on first call of useEffect and than stays stale.
To correct this you should either completely remove second argument of useEffect or make array contain [count].
Callback is working as it receives previous count value as first argument.
So correct code will look like
function Tiker() {
var [count, setCount] = useState(0);
useEffect(() => {
var timerID = setInterval(_=>
setCount(count + 1)
, 1000);
return function cleanup() {
clearInterval(timerID);
};
}, [count]); // Put variable that useHook depends on
return <div>
this is ticker
<button onClick={() =>
setCount(count + 1) //setCount(count+1) does work
}>up </button>
{count}
</div>
}
It appears that using a callback is the easiest way to change state when called from setInterval. as evident by Frodor comment above and from https://overreacted.io/making-setinterval-declarative-with-react-hooks/

Resources