4

we are talking about functional components having useState

lets say

const [age, setAge] = useState(0)

now let us say while updating age I have to use the previous age

React docs mention something called FUNCTIONAL UPDATES where you can pass a function and the argument to that will be the previous value of the state, eg.

setState((previousAge) => previousAge + 1)

why do I need to do this when I can just do

setState(previousAge + 1)

what are the benefits of using functional setState,

I know in class-based components there was something called batching of state updates in the functional way, but I can't find anything like that in functional components documentation.

ashish singh
  • 6,526
  • 2
  • 15
  • 35
  • Using a function allows React to internally optimize the resolution of many setState calls. For simple applications this would not be necessary. But, to future proof an app that may grow very large, it may be a good choice to start all your setters in this way. – Randy Casburn Dec 13 '20 at 15:53
  • Just for clarity, the concept of "functional updates" (functional programming) implies certain things. You should know that internally, react state objects are actually stored as a series of [memoized functions](https://en.wikipedia.org/wiki/Memoization). So by using a function as your setState parameter, you are simply adding another pure function to the stack of memoized functions. – Randy Casburn Dec 13 '20 at 16:02
  • See e.g. https://stackoverflow.com/questions/53024496/state-not-updating-when-using-react-state-hook-within-setinterval or https://stackoverflow.com/questions/56782079/react-hooks-stale-state for examples – Bergi Dec 13 '20 at 18:51

3 Answers3

3

They are not the same, if your update depends on a previous value found in the state, then you should use the functional form. If you don't use the functional form in this case then your code will break sometime.

Why does it break and when

React functional components are just closures, the state value that you have in the closure might be outdated - what does this mean is that the value inside the closure does not match the value that is in React state for that component, this could happen in the following cases:

1- async operations (In this example click slow add, and then click multiple times on the add button, you will later see that the state was reseted to what was inside the closure when the slow add button was clicked)

const App = () => {
  const [counter, setCounter] = useState(0);

  return (
    <>
      <p>counter {counter} </p>
      <button
        onClick={() => {
          setCounter(counter + 1);
        }}
      >
        immediately add
      </button>
      <button
        onClick={() => {
          setTimeout(() => setCounter(counter + 1), 1000);
        }}
      >
        Add
      </button>
    </>
  );
};

2- When you call the update function multiple times in the same closure

const App = () => {
  const [counter, setCounter] = useState(0);

  return (
    <>
      <p>counter {counter} </p>
      <button
        onClick={() => {
          setCounter(counter + 1);
          setCounter(counter + 1);
        }}
      >
        Add twice
      </button>
   
    </>
  );
}
ehab
  • 7,162
  • 1
  • 25
  • 30
1

Problems might occur depending on how fast/often your setter gets called.

If you are using the simple way by getting the value from the closure, subsequent calls between two renders might not have the desired effect.

A simple example:

function App() {
    const [counter, setCounter] = useState(0);
    const incWithClosure = () => {
        setCounter(counter + 1);
    };
    const incWithUpdate = () => {
        setCounter(oldCounter => oldCounter + 1);
    };

    return (<>
        <button onClick={_ => { incWithClosure(); incWithClosure(); }}>
            Increment twice using incWithClosure
        </button>
        <button onClick={_ => { incWithUpdate(); incWithUpdate(); }}>
            Increment twice using incWithUpdate
        </button>
        <p>{counter}</p>
    </>);
}

Both buttons calls one of the increment methods twice. But we observe:

  • The first button will increment the counter only by 1
  • The second button will increment the counter by 2, which is probably the desired outcome.

When can this happen?

  • Obviously, if incWithClosure is called multiple times immediately after each other
  • If asynchronous tasks are involved, this can easily happen (see below)
  • Perhaps, if React has much work to do, its scheduling algorithms may decide to handle multiple very fast clicks using the same event handler

Example with asynchronous work (simulating loading a resource):

function App() {
  const [counter, setCounter] = useState(0);
  const incWithClosureDelayed = () => {
    setTimeout(() => {
      setCounter(counter + 1);
    }, 1000);
  };
  const incWithUpdateDelayed = () => {
    setTimeout(() => {
      setCounter((oldCounter) => oldCounter + 1);
    }, 1000);
  };

  return (
    <>
      <button onClick={(_) => incWithClosureDelayed()}>
        Increment slowly using incWithClosure
      </button>
      <button onClick={(_) => incWithUpdateDelayed()}>
        Increment slowly using incWithUpdate
      </button>
      <p>{counter}</p>
    </>
  );
}

Click on the first button twice (within one second) and observe that the counter gets incremented only by 1. The second button has the correct behavior.

Hero Wanders
  • 3,237
  • 1
  • 10
  • 14
0

Because if you don't you will find at some point that you get an old value for age. The problem is, sometimes what you suggest will work. But sometimes it will not. It may not break in your current code today but it may break in a different code you wrote a few weeks ago or your current code a few months from now.

The symptom is really, really weird. You can print the value of the variable inside a jsx component using the {x} syntax and later print the same variable using console.log after rendering the jsx component (not before) and find that the console.log value is stale - the console.log that happens after the render can somehow have older value than the render.

So the actual value of state variables may not always work correctly in regular code - they are only designed to return the latest value in a render. For this reason the callback mechanism in a state setter was implemented to allow you to get the latest value of a state variable in regular code outside of a render.

slebetman
  • 109,858
  • 19
  • 140
  • 171