Normally when we need to update a state in a functional component, we do something like this:
function Example() {
const [count, setCount] = React.useState(0);
return (<div><p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>);
}
When and why will we ever need to use the functional update form?
function Example() {
const [count, setCount] = React.useState(0);
return (<div><p>You clicked {count} times</p>
<button onClick={() => setCount(c=>c + 1)}>
Click me
</button>
</div>);
}
Use the function form when the setter may close over an old state value.
For example, if an async request is initiated, and you want to update state after that's done, the request that was made will have scope of the state as it was at the beginning of the request, which may not be the same as the most up-to-date render state.
You may also need to use the function form if the same state value was just updated, eg
setValue(value + 1);
// complicated logic here
if (someCondition) {
setValue(value => value + 1);
}
because the second call of setValue closes over an old value.
State Updates May Be Asynchronous:
https://reactjs.org/docs/state-and-lifecycle.html#state-updates-may-be-asynchronous
useState is the same as setState in this condition.
You can see the different when call set state twice:
<button
onClick={() => {
setCount(count + 1);
setCount(count + 1);
}}
></button>;
<button
onClick={() => {
setCount(c => (c + 1));
setCount(c => (c + 1));
}}
></button>;
There are other use cases too. For example, when you call useState inside an effect. If new state is dependent on old state, this might cause an infinite loop.
useEffect(() => {
setCounter(counter + 1);
}, [counter]);
You can avoid this by using functional updates:
useEffect(() => {
setCounter(old => old + 1);
}, []);
According to this: https://reactjs.org/docs/state-and-lifecycle.html#state-updates-may-be-asynchronous
The the functional update form make sure that the previous state that you take reference from is the latest / finalized version when there might be multiple setState hook (which is asynchronous) called (for example, if the user spam click on the button)
And also due to its async nature, the state will not be updated right away within a function, for e.g:
func() {
console.log(counter) // counter = 1
setCounter(counter => counter + 1) // counter = 1
console.log(counter) // counter = 1
}
The functional update form also allows the update function to be passed to its children while still having access to the parent’s state.
function MyButton(props){
// return <button onClick={()=>props.onClick(count+1)}>+1</button>; // error as count is not exposed here
return <button onClick={()=>props.onClick(n=>(n+1))}>+1</button>;
}
function Example() {
const [count, setCount] = React.useState(0);
return (<div><p>Counter: {count}</p>
<MyButton onClick={setCount}/>
</div>);
}
ReactDOM.render(<Example/>,document.querySelector("div"));
Related
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.
When load more is clicked, count value is not getting updated only on the second click it is getting updated.
Expectation is on load more button click value should be passed as 2, but now it is sending as 1.
what I'm doing wrong here. Please guide
Below is the sample code.
const [count, setCount] = useState(1);
fetchData() {
.....
}
loadMore() {
....
fetchData()
}
Render HTML Method:
<button onClick={ () => {
setCount(count + 1);
loadMore();
}}
use this -
const handleOnClick = () => {
setCount((count) => count + 1);
loadMore();
}}
<button onClick={handleOnClick}>Click me</button>
You may check the answer here in this react documentation.
I’ve updated the state, but logging gives me the old value.
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.
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
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/