import {useEffect,useState} from 'react';
export default function App() {
const [count,setCount]=useState(0);
const [flag,setFlag]=useState(false);
function increment(){
setCount(prevState=>{
if(flag)
return prevState
return prevState 1;
});
}
useEffect(function(){
increment();
setFlag(true);
increment();
},[]);
return (
<div className="App">
{count}
</div>
);
}
Was playing around with effects and states in reatct functional component, I expected the code to output "1" but it's giving the output as "2", Why is it happening and How can I make it print 1 ?
CodePudding user response:
Once you call setFlag
, React will update the returned value of your useState
call [flag,_] = useState()
on the next render.
Your setFlag(true)
call schedules a re-render, it doesn't immediately update values in your function.
Your flag
is a boolean const
after all -- it can't be any value but one value in that function call.
How to solve it gets interesting; you could put the the flag
inside of a single state object i.e. useState({count: 0, flag: false})
But more likely, this is an academic problem. A count increment sounds like something that would trigger on a user interaction like a click
, and so long as one function doesn't call increment() multiple times (this sounds unusual), the re-render will happen in time to update your flag
state.
CodePudding user response:
For performance reasons, React defers useState
hook updates until function completes its execution, i.e. run all statements in the function body and then update the component state, so React delays the update process until a later time.
Thus, when increment
function execution is completed, React updates the state of count
. But for setFlag
method, the execution environment is a context of useEffect
hook's callback, so here React's still waiting for a completion of useEffect's callback function. Therefore, inside the callback of useEffect
the value of flag
is still false
.
Then you again called your increment
function and when this function finished its execution, your count
again was incremented by 1
.
So, in your case, the key factor is the way of deferring state updates until function execution by React.
Think of setState() as a request rather than an immediate command to update the component. For better perceived performance, React may delay it, and then update several components in a single pass. React does not guarantee that the state changes are applied immediately.
setState() does not always immediately update the component. It may batch or defer the update until later. This makes reading this.state right after calling setState() a potential pitfall. Instead, use componentDidUpdate or a setState callback (setState(updater, callback)), either of which are guaranteed to fire after the update has been applied. If you need to set the state based on the previous state, read about the updater argument below.
For more information, you can also read about Batch updates
in React (especially in React 18) or Reactive programming
(this is not React), where the main idea is real-time or timely updates.
CodePudding user response:
For a better understanding I would think it of as replacing the invocation directly with setter and we know how the state batching works so ...
import { useEffect, useState } from "react";
export default function App() {
const [count, setCount] = useState(0);
const [flag, setFlag] = useState(false);
useEffect(function () {
// increment(); becomes below
setCount((prevState) => {
if (flag) return prevState;
return prevState 1;
});
// queued update count returns
// count => count 1 0 0 1 = 1
setFlag(true);
//set flag=true in next render
// increment(); becomes below
setCount((prevState) => {
if (flag) return prevState;
return prevState 1;
});
// so flag is still false here and count is 1
// queued update count returns
// count => count 1 1 1 1 = 2
// done and count for next render is 2 and flag will be false
}, []);
return <div className="App">{count}</div>;
A better explaination in Docs - Queueing state updates and state as snapshot
CodePudding user response:
State updates are "batched". See the other answers for an explanation. Here's a workaround using useRef
- since a ref can be updated during this render, you can use it like a "normal" variable.
const { useState, useRef, useEffect } = React;
function App() {
const [count, setCount] = useState(0);
const flag = useRef(false);
function increment() {
setCount(prevState => {
if (flag.current)
return prevState;
return prevState 1;
});
}
useEffect(function() {
increment();
flag.current = true;
increment();
}, []);
return <div className="App">{count}</div>;
}
ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.production.min.js"></script>
<div id="root"></div>