简体   繁体   中英

Why is passing function argument to React.useState and the return function will return stale value

Give the codes below

import { useState } from "react";
import "./styles.css";

export default function App() {
  const [count1, setCount1] = useState(0);
  const [count2, setCount2] = useState(0);

  const inc1 = () => {
    console.log("debug inc:", count1);
    setCount1((prev) => prev + 1);
  };

  const inc2 = () => {
    console.log("debug inc2:", count2);
    setCount2(count2 + 1);
  };

  const [processInc1] = useState(() => {
    console.log("debug longProcessBeforeInc:", count1);
    // Run some long process here
    return inc1;
  });

  const [processInc2] = useState(() => {
    console.log("debug longProcessBeforeInc:", count2);
    // Run some long process here
    return inc2;
  });

  console.log("debug render:", count1, count2);

  return (
    <div className="App">
      <h3>
        {count1} - {count2}
      </h3>
      <button onClick={inc1}>Inc 1</button>
      <br />
      <br />
      <button onClick={inc2}>Inc 2</button>
      <br />
      <br />
      <button onClick={processInc1}>Long Inc 1</button>
      <br />
      <br />
      <button onClick={processInc2}>Long Inc 2</button>
    </div>
  );
}

inc1 , inc2 , processInc1 all works as expected where you increase value by 1 and it renders correctly.

So with inc1 , inc2 the main difference is setCount1((prev) => prev + 1); and setCount2(count2 + 1); , and processInc1 , processInc2 basically return inc1 and inc2 respectively via useState first time the component renders.

I understand from here https://reactjs.org/docs/hooks-faq.html#why-am-i-seeing-stale-props-or-state-inside-my-function that it has something to do with closure but given the example above I fail to wrap my head around why inc2 , and processInc1 works but not processInc2 ?

Here is the link to the codesandbox for the abovehttps://codesandbox.io/s/eloquent-morse-37xyoy?file=/src/App.js

You are on the right path. As you were saying, this problem is actually caused by the use of the closure. So let's start by showing you a nice definition of closure:

A closure is the combination of a function and the lexical environment within which that function was declared. This environment consists of any local variables that were in-scope at the time the closure was created.

Docs: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Closures

This means that when you return the closure inc2 in the useState, you are also creating a copy of the variables that are used at that particular time (ie initial rendering) by that lexical environment (this includes the count2 value). This is why processInc2 will keep having the same old count2 value.

At the same time processInc1 function will work correctly because, by using a callback in the useState, you are always getting the current value of the count1 state.

Lastly, inc2 works because you are directly calling it when you click the button, so the count2 value gets evaluated at that moment that you call it (therefore it will most probably have the current value).

A very important detail here is that inc1 and inc2 are re-defined each time the App component renders.

function App() {
  // ...

  const inc1 = () => {
    console.log("debug inc:", count1);
    setCount1((prev) => prev + 1);
  };

  // ...
}

You then store those 2 functions inside a state, that does never change.

const [processInc1] = useState(() => {
  console.log("debug longProcessBeforeInc:", count1);
  // Run some long process here
  return inc1;
});

This will result in processInc1 and processInc2 that point to the very first definition of inc1 and inc2 (created on the first render).

The reason count1 and count2 never update in this first version of the function is because the variable(s) are never re-assigned. This is by design.

The only reason count1 and count2 change in a future render is because useState() will return the new value. After you receive this new value inc1 and inc2 are re-defined.

processInc1 and processInc2 are then pulled out of a React state that holds the first definition of inc1 and inc2 , so usage of count1 and count2 inside those functions will refer to the first value of count1 and count2 .

When you do setCount2(count2 + 1) inside inc2 and call it via processInc2 the value of count2 is still 0 and will never change. This is because processInc2 refers to the very first definition of inc2 , and not the current definition.

setCount1((prev) => prev + 1) works due to the different function signature. Where inc2 passes a static value ( 0 + 1 ) to the setter, inc1 passes a transformation (as a callback). When you pass a function to setCount1 React will call that function with the current state as the sole argument. The return value is then used as the new state. So although processInc1 still uses the very first definition of inc1 . It will always be relevant, since it describes the transformation that must be made rather than the value that must be set.

Do note that console.log("debug inc:", count1); will keep logging 0 when called via processInc1 for the above reasons.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM