關於setState使用的一些坑


setState更新數組

你會發現,如果直接使用push等方法改變state,按理來說,push會改變原數組,數組應該更新,但渲染出來的state並不會更改

let newValue = 1;
const [array, setArray] = useState([]);
const handleChange = (newValue: number) =>{
	array.push(newValue);
	setState(array);//array更新了,但無法觸發渲染
	console.log(array);//[1]
	//array增加了newValue,但渲染並未發生改變
}

render:
<p>This array is {JSON.stringify(array)}</p> //[]

這是由於js中,數組的賦值是引用傳遞的,array.push相當於直接更改了數組對應的內存塊,但react內部用於對比的array的內存並沒有更改,是指向同一個內存的,setState只做shallow compare,因此沒有觸發re-render。
可以使用擴展運算符,創建一個新數組,更改內存引用

const handleChange = (newValue: number) =>{
	const newArray = [...array, newValue];
	setState(newArray);//此處本質上是改變了引用
	console.log(array);//[]
	//array並未改變,但渲染改變了
}

render:
<p>This array is {JSON.stringify(array)}</p> //[1]

或者觸發展示組件的re-render,這樣即使不改變數組的引用,依然可以正確顯示變動。

const handleChange = (newValue: number) =>{
	setValue(newValue);
	setState(array.push(newValue));//其他更新觸發了組件的re-render,此時可以正常顯示變動
	console.log(array);//[1]
	//array改變,且渲染改變
}

render:
<p>This array is {JSON.stringify(array)}</p> //[1]

再給一個直觀的例子(感謝我的同事@ling)
直接嘗試:https://codepen.io/ling-cao/pen/NWrMRrq

const { useRef, useEffect, useState } = React

const useMemoryState = (init) => {
  const [arr, setArr] = useState(init)
  const lastArrRef = useRef(null)
  const updateArr = next => {
    lastArrRef.current = [...arr];
    console.log(next);
    setArr(next)
  }
  return [arr, updateArr, lastArrRef.current]
}

let i = 0;
const App = () => {
  const [arr, setArr, lastArr] = useMemoryState([0])
  const [updateSign, setUpdateSign] = useState(false)
  
  return(
    <>
      <div className="text"><label>Current array :</label> {JSON.stringify(arr)}</div>
      <div className="box-container">
        <div className="box">
          <h1>Push a number to array</h1>
          <pre>setArr(arr.push(i) && arr)</pre>
          <br />
          <button
            onClick={() => {
              i++;
              setArr(arr.push(i) && arr)
            }}
            className="btn btn-2 btn-2c">
              Try it
           </button>
        </div>
        <div className="box">
          <h1>Push a number to array and renew array</h1> 
          <pre>setArr(arr.push(i) && [...arr])</pre>
          <br />
          <button
            onClick={() => {
              i++;
              setArr(arr.push(i) && [...arr])
            }}
            className="btn btn-2 btn-2c">
              Try it
           </button>
        </div>
        <div className="box">
          <h1>Push a number to array and update another state</h1>
          <pre>setArr(arr.push(i) && arr); setUpdateSign(x => !x)</pre>
          <br />
          <button
            onClick={() => {
              i++;
              {
                setArr(arr.push(i) && arr)
                setUpdateSign(x => !x)
              }
            }}
            className="btn btn-2 btn-2c">
              Try it
           </button>
        </div>
      </div>
      </>
  );
}

逐次點擊第二個按鈕或第三個按鈕都可以正常更新渲染。

點擊第一個按鈕,通過console可以看出來,array數組值有更新,但沒有渲染(Current array 沒變);再點其他兩個按鈕時,會把第一個按鈕點擊更新的結果一起渲染出來。

側面展示並不是沒有更新數組,而是更新后未渲染。

setState不會立即改變數據

setState某種意義上是類似於異步函數的。

// name is ""
this.setState({
    name: "name"
})
console.log(`name is ${this.state.name}`)

這樣寫,name是不能正常顯示。
最常用的辦法就是使用回調函數

this.setState({
    name: "name"
}, () => {
  console.log(`name is ${this.state.name}`)
})

多個setState的更新

setState的“異步”是本身執行的過程和代碼是同步的,只是合成事件和鈎子函數的調用順序在更新之前,導致在合成事件和鈎子函數中沒辦法立馬拿到更新后的值,形成了所謂的異步。批量更新優化也是建立在“異步”之上的,如果對同一個值進行多次setState,setState的批量更新策略會對其進行覆蓋,取最后一次執行;如果是同時setState多個不同的值,在更新時會對其合並批量更新。

setState異步回調獲取不到最新值

  useEffect(() => {
    const newModel = {
      name: props.name,
      datasetId: props.datasetId,
      modelId: null,
      trainingStatus: TrainingStatus.Init,
      modelStatus: Status.NotStarted,
    } as TrainingModel;
    setModels([...models, newModel]);
    startTraining(newModel);
  }, [props.datasetId]);

  const startTraining = async (newModel: TrainingModel) => {
    const dataset = await getDataset(newModel.datasetId);
    let newModels = [...models];
    let currModel = newModels.find(x => x.datasetId == newModels.datasetId);
    currModel.trainingStatus = TrainingStatus.CreateDataset;
    //此時可通過頁面的渲染效果知道models中已有值,但此處斷點models為空
    setModels(newModels);
  };

類似的,老生常談的,在useEffect里面設置一個Interval,過了Interval time,也同樣是useEffect更新時的state值,而得不到最新的state值。
為解決異步導致的獲取不到最新state的問題,使用setState的回調函數獲取state的當前最新值

  const startTraining = async (newModel: TrainingModel) => {
    const dataset = await getDataset(newModel.datasetId);
      setModels(lastModels => { //此時的lastModels是models的最新值
        const nextModels = [...lastModels];
        let currModel = nextModels.find(x => x.datasetId == newModel.datasetId);
        currModel.trainingStatus = TrainingStatus.CreateDataset;
        return nextModels;
      });
  };

原因是,組件內部的任何函數,包括事件處理函數和effect,都是從它被創建的那次渲染中被[看到]的,也就是說,組件內部的函數拿到的總是定義它的那次渲染中的props和state。想要解決,一般兩種方法,一種是上述的使用setState回調函數獲取state最新值,一種是使用ref保存修改並讀取state。


免責聲明!

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



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