- Published on
「React筆記」正確且安全的更新 state 內的 Arrays/Objects
- Authors
- Name
- Wen Chen
前言
這是一篇 debug 筆記,最近在替同事 code review 時,他卡在一個 bug 裡面好久,最後才發現原來一切是 state,繞了一大圈才發現問題出在同事去對存放陣列的 state 做 push , 異動完後再 setState 回去,導致畫面沒法正常更新。
看到這裡先別噴,確實在我個人撰寫 React 的生涯中,因為一再謹記 「請永遠把 state 當作是 immutable 的!」,所以並沒有發生過類似的錯誤,但當同事問到:
可是我在 push 完資料後看 log 出來的資料是有被異動進去的耶
當下卻一時沒法馬上反應過來,經過一些搜尋,才得到了答案,這篇也算是對底層概念的加強。
請永遠把 state 當作是 immutable 的!
首先我們先看看React官方文件怎麼說
Arrays are mutable in JavaScript, but you should treat them as immutable when you store them in state. Just like with objects, when you want to update an array stored in state, you need to create a new one (or make a copy of an existing one), and then set state to use the new array.
再來我們先看一下簡化版的情境,因為過去沒有做過這件事,所以一時之間也無法反應過來,但實際上出問題的 Code 是長這樣:
import { useState } from 'react';
function App() {
const [list, setList] = useState([1]);
const handleButtonClick = () => {
list.push(2)
setList(list)
};
return (
<div>
{list.map(value => <p>{value}</p>)}
<button onClick={handleButtonClick}>add</button>
</div>
)
}
先解釋一下真實情境,實際上這功能在每次 click
之後會去撈取特定 api 資料,拿到資料後再經過一系列的轉換,將符合前端畫面的資料,塞回原本已經存在的 list
因為中間做了很多資料結構的轉換,所以在一開始除錯的時候,我們花了大把時間在確認「轉換」的 function 行為是否異常導致元件無法正確更新,直到最後一籌莫展時,才發現魔鬼居然藏在最後的 setState
中
接著我們回到上面的案例,當事人記得 「 觸發畫面 re-render 需要 setState 」,但卻忘記最重要的 immutable,而他在先前 debug 時,表示曾在下面塞過 log,確實 Array 也有被異動:
import { useState } from 'react';
function App() {
const [list, setList] = useState([1]);
const handleButtonClick = () => {
list.push(2)
console.log(list) // (2) [1, 2]
setList(list)
};
return (
<div>
{list.map(value => <p>{value}</p>)}
<button onClick={handleButtonClick}>add</button>
</div>
)
}
欸不是!我傳進去的資料確實不一樣啊
setState 的畫面更新機制
經過一番搜尋,我們可以從 React 的底層邏輯來解釋這樣子的行為,底下附上資料來源,這裡僅提下重點:
React 在 setState
時會先以 Object.is()
來檢查新舊 state 有無變更,如果相同的話則不會做更新
到此也就真相大白了,當我們直接去 mutate state 時,就算 state 本身的值已經被改變,但在傳入 setState
時因為根本上物件的位址相同,所以並不會觸發後續的動作,進而導致畫面沒有任何更新
推薦閱讀: React 畫面更新的核心機制(下):Reconciliation
如何正確的更新儲存在 state 內的 Array / Object
回到最初 React 官方文件中提到的:
當你要更新 state 中的陣列 (物件) 時,你應該造一個新的 (或複製現有的),並 setState 新的陣列 (物件)
總之面對陣列(物件),我們永遠應該用新的去取代舊的:
import { useState } from 'react';
function App() {
const [list, setList] = useState([1]);
const handleButtonClick = () => {
const clone = [...list];
clone.push(2);
setList(clone);
};
return (
<div>
{list.map(value => <p>{value}</p>)}
<button onClick={handleButtonClick}>add</button>
</div>
)
}
將程式碼改掉後,行為便恢復正常了,當然範例只有用擴展符,要用 lodash
的 cloneDeep
之類的也都是沒問題的,只要確保是複製一個新的就好。
CodeSandBox,因為是示意,這裡簡易用 index 解決 key warning 請見諒
後記
雖說 「 永遠把 state 當作是 immutable 的 」 是所有 React 初學者接觸到 state 時就會閱讀到的金句,但當時卻從沒想過根本的原因,也算是補強了一些更底層的知識。