Home > Back-end >  Related to weird behaviour of react useState and useEffect
Related to weird behaviour of react useState and useEffect

Time:10-29

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.

React Component: setState()

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>

  • Related