這篇文章介紹了 mutation、immutable 的概念,以及 React 為什麼要用 immutable 的資料結構。簡而言之,React 的 setState 會用 Object.is() (一種類似 === 的淺層比較) 來判斷 state 是否有變化,並決定是否要重新渲染。所以,如果只是修改 state 的內容,而沒有改變它的參考位置,React 就不會重新渲染。這意味著,我們不能直接改變 state 來觸發重新渲染,而是要用 setState 給它一個新的值。這就是 React 需要 immutable 的原因,因為我們要用複製和修改的方式來產生一個新的物件或陣列,讓 React 能夠發現 state 的變化。
Mutation in JavaScript
Object 和 Array 是 JavaScript 中最常見可以被「mutate」的資料結構。Mutation 指的是直接改變物件或陣列的內容,而不是修改它的參考位置。例如我可以修改一個物件的屬性,為 user 加上一個 name 屬性。
const user = { name: "Brad" };
user.name = "Jay"; // 修改了 user 這個物件的 name 屬性
我們也可以透過 push 或 splice 等方法來直接修改陣列的內容。
const arr = [1, 2, 3];
arr.push(4); // 加入一個 4,變成 [1, 2, 3, 4]
arr.splice(1, 1); // 從索引 1 開始,刪除 1 個元素,變成 [1, 3, 4]
Mutation 和 const 關鍵字沒有關係,因為 const 只是防止變數被重新指定,而不是防止變數的內容被改變。所以我們可以修改 const 物件的內容,但不能重新指定它。
const user = { name: "Brad" };
user = { name: "Jay" }; // 這樣會報錯,因為 user 不能被重新指定
user = 0; // 這樣也會報錯,因為 user 不能被重新指定
const arr = [1, 2, 3];
arr = [1, 2, 3, 4]; // 這樣會報錯,因為 arr 不能被重新指定
Immutability
透過 mutations 直接修改陣列或物件並沒有錯,但是有些人認為使用 mutation 修改資料,長期下來可能會產生一些 bugs,所以他們會選擇使用 immutable strategy 來處理資料。Immutable strategy 指的是,我們不會直接修改物件或陣列,而是複製一份,然後修改複製的資料,最後回傳複製的資料。以下是一個 mutable 和 immutable 的範例。
const user = { id: 1, name: "Brad" };
// mutable
user.name = "Jay";
// immutable - 這裡我們使用了展開運算子,複製了 user 並加上 name 屬性
const newUser = { ...user, name: "Jay" };
// immutable - 這裡我們使用了 Object.assign,複製了 user 並加上 name 屬性
const newUser2 = Object.assign({}, user, { name: "Jay" });
JavaScript 中的 primitive types (例如 number, string, boolean) 都是 immutable 的,所以它們被修改一定都是透過重新指定變數的方式。
let num = 1;
num = 2; // 這樣就是重新指定 num,所以 num 會變成 2
Why is React state immutable?
現在我們知道什麼是 mutable 和 immutable,就可以清楚地解析下面的 React 範例為什麼會有問題。在下面的範例中,雖然對 items 的 push (mutation) 是成功的,但由於 React 用 Object.is() 來判斷 state 是否有變化,所以 push 後的 items 對 React 來說是一樣的,因此 React 不會更新 state,也不會重新渲染。
如果我們要讓 React 重新渲染,就必須使用 immutable 的方式來更新 state。透過展開運算子,我們可以複製一份 items,然後對複製的 items 添加一個元素 (類似於 items.push(4)),最後再回傳複製的 items。因此,React 會判斷到 state 有變化,所以會重新渲染。
Primitives in React state
如果我們直接修改 primitive 類別的 state,React 其實會重新渲染!這是因為 count++ 改變了 count 的參考位置,然後 React 判斷 state 有變化,所以更新了 state 並重新渲染。
但我們不應該使用 count++,因為這可能會讓人誤會 mutations 是可以的。我們應該使用 setCount(count + 1),這樣比較清楚地表達我們不會直接修改 state,而是透過傳入新的值來重新渲染。所以我們應該換一個更精確的說法,就是「不要直接修改 state」。
我們更不該直接修改 props,因為 props 是 immutable 的。Props 通常是由父元件傳入,但是直接修改 props 並不會讓 React 重新渲染,可能讓父元件的 state 與子元件的 props 不一致,最終導致預期的結果與實際結果不一致。
const ItemList = ({ items }) => {
items.push(4); // this is not allowed
return <>{items}</>;
};