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。
