useGetState custom hooks solve the problem that useState asynchronous callback cannot obtain the latest value

Two ways to pass parameters to setState

1. Directly pass in the new value setState(options);

const [state, setState] = useState(0);
setState(state + 1);

2. Pass in the callback function setState(callBack);

const [state, setState] = useState(0);
setState((prevState) => prevState + 1); // prevState 是改变之前的 state 值,return 返回的值会作为新状态覆盖 state 值

useState asynchronous callback cannot obtain the latest value and solution

Under normal circumstances, setState can directly use the first method above to pass parameters, but in some special cases, the first method will cause exceptions; for example, if you want to get the latest status and set the status in an asynchronous callback or closure, then the first The status obtained in this way is not real-time. React official documentation mentions: Any function inside the component, including event handling functions and Effect, is "seen" from the rendering in which it was created, so the referenced value It is still old, and finally causes an exception in setState:

import React, { useState, useEffect } from 'react';

const App = () => {
  const [arr, setArr] = useState([0]);

  useEffect(() => {
    console.log(arr);
  }, [arr]);

  const handleClick = () => {
    Promise.resolve().then(() => {
      setArr([...arr, 1]); // 此时赋值前 arr 为:[0]
    })
      .then(() => {
        setArr([...arr, 2]); // 此时赋值前 arr 为旧状态仍然为:[0]
      });
  }

  return (
    <>
      <button onClick={handleClick}>change</button>
    </>
  );
}

export default App;

// 输出结果
[0]
[0,1]
[0,2]

In the above code, the App component is actually a closure function. handleClick references arr. The value of arr is indeed updated after the first setArr. We can also see in the above output result, but the handleClick event processing function executed this time The scope is still old, and the arr referenced in it is still old, causing the result after the second setArr to be [0, 2]:

In the class component, we can use setState(options, callBack); setState is performed again in the second parameter callback function of setState. There is no closure scope problem, but useState in React Hook removes the second parameter of setState. Parameters, and it is not good if there are too many nestings;

Solution 1

const handleClick = () => {
    Promise.resolve().then(() => {
      setArr(prevState => [...prevState, 1]); // 这里也可以不改,使用第一中传参方式 setArr([...arr, 1]); 因为这里不需要获取最新状态
    })
      .then(() => {
        setArr(prevState => [...prevState, 2]); // 这里必须改成回调函数传参方式,否则会读取旧状态,导致异常
      });
  }


// 输出结果
[0]
[0,1]
[0,1,2]

We found that when passing parameters in callback mode, the output result is correct.

Solution 2:

Use useReducer to imitate forceUpdate in class components to implement forced rendering of components; Note: This solution is only applicable when only the page depends on the data. If there are hooks like useEffect that cannot capture the changes in real time when monitoring the data (arr in the example)

import React, { useState, useReducer } from 'react';

const App = () => {
  const [arr, setArr] = useState([0]);
  const [, forceUpdate] = useReducer(x => x + 1, 0);

  const handleClick = () => {
    Promise.resolve().then(() => {
      arr.push(1); // 如果这里也需要做一次渲染在改变状态后调用 forceUpdate() 即可
    })
      .then(() => {
        arr.push(2);
        forceUpdate();
      });
  }

  return (
    <>
      <h1>{arr.toString()}</h1>
      <button onClick={handleClick}>change</button>
    </>
  );
}

export default App;

Solution 3:

Using ref, the state changes and the value is mapped to ref. The change of ref will not trigger a page update, but the latest value can be obtained asynchronously, so if it needs to be used on the page, use state, and if it is used in asynchronous logic, use ref

import React, { useState, useRef, useEffect } from 'react';

const App = () => {
  const [arr, setArr] = useState([0]);
  let ref = useRef();
  useEffect(() => {
    ref.current = arr;
    console.log(arr);
  }, [arr]);

  const handleClick = () => {
    Promise.resolve().then(() => {
      const now = [...ref.current, 1];
      ref.current = now;
      setArr(now);
    })
      .then(() => {
        setArr([...ref.current, 2]);
      });
  }

  return (
    <>
      <h1>{arr.toString()}</h1>
      <button onClick={handleClick}>change</button>
    </>
  );
}

export default App;

Option 4, recommended option:

Methods such as Example 3 above can encapsulate a hook to associate state and ref, and at the same time provide a method for obtaining the latest value asynchronously, for example:

const useGetState = (initVal) => {
  const [state, setState] = useState(initVal);
  const ref = useRef(initVal);
  const setStateCopy = (newVal) => {
    ref.current = newVal;
    setState(newVal);
  }
  const getState = () => ref.current;
  return [state, setStateCopy, getState];
}

const App = () => {
  const [arr, setArr, getArr] = useGetState([0]);
  useEffect(() => {
    console.log(arr);
  }, [arr]);

  const handleClick = () => {
    Promise.resolve().then(() => {
      setArr([...getArr(), 1]);
    })
      .then(() => {
        setArr([...getArr(), 2]);
      });
  }

  return (
    <>
      <h1>{arr.toString()}</h1>
      <button onClick={handleClick}>change</button>
    </>
  );
}

When the useState asynchronous callback cannot obtain the latest value, we recommend solution 1 and solution 4. Solution 1 is simpler and solution 4 is more perfect. Both are recommended, depending on your usage scenario.

Guess you like

Origin blog.csdn.net/weixin_44786530/article/details/132792343