React Hook 中 useState 異步回調獲取不到最新值及解決方案


預先了解 setState 的兩種傳參方式

1、直接傳入新值 setState(options);

列如:

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

2、傳入回調函數 setState(callBack);

例如:

const [state, setState] = useState(0);
setState((prevState) => prevState + 1); // prevState 是改變之前的 state 值,return 返回的值會作為新狀態覆蓋 state 值

useState 異步回調獲取不到最新值及解決方案

通常情況下 setState 直接使用上述第一種方式傳參即可,但在一些特殊情況下第一種方式會出現異常;
例如希望在異步回調或閉包中獲取最新狀態並設置狀態,此時第一種方式獲取的狀態不是實時的,React 官方文檔提到:組件內部的任何函數,包括事件處理函數和 Effect,都是從它被創建的那次渲染中被「看到」的,所以引用的值任然是舊的,最后導致 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;

上面代碼,App 組件實際也是個閉包函數,handleClick 里面引用着 arr,第一次 setArr 后 arr 的值確實更新了,我們也可以在下面截圖中看到,但此次執行的 handleClick 事件處理函數作用域還是舊的,里面引用的 arr 仍然為舊的,導致第二次 setArr 后結果為 [0, 2]:

在 class 組件中我們可以使用 setState(options, callBack); 在 setState 的第二個參數回調函數中再次進行 setState,也不存在閉包作用域問題,但是 React Hook 中 useState 移除了 setState 的第二個參數,而且若嵌套太多也不佳;

解決方案1(推薦使用):

將上述代碼使用第二種(回調)方式傳參

const handleClick = () => {
    Promise.resolve().then(() => {
      setArr(prevState => [...prevState, 1]); // 這里也可以不改,使用第一中傳參方式 setArr([...arr, 1]); 因為這里不需要獲取最新狀態
    })
      .then(() => {
        setArr(prevState => [...prevState, 2]); // 這里必須改成回調函數傳參方式,否則會讀取舊狀態,導致異常
      });
  }

解決方案2:

使用 useReducer 仿造類組件中的 forceUpdate 實現組件強制渲染;

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;

點擊前:

點擊后:

解決方案3:

利用 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);
  });

  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;

最后附上官方相關文檔:為什么我會在我的函數中看到陳舊的 props 和 state ?


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM