簡單介紹 immer。
在 react 專案裡面我們會希望可以確保所有狀態都是 immutable 的,盡量去避免直接操作現有的變數,如果有需要更改狀態,就重新建立一個新的 array 或是 object。
因為在 react 裡面大多數的比較跟判斷都是 Object.is()
來執行的,如果直接用 someArray.push()
或是 someObject.key = "value"
的方式來修改變數的話有可能會造成 react 沒有正確更新畫面,造成錯誤,所以我們會盡量確保所有的變數跟改動都是 immutable 的。
但是當遇到較為複雜的狀態時 immutable 真的很麻煩,而且非常影響程式碼的可讀性。
用 immer 官方文件的範例來看,假設有一個 array 的 todo list,裡面放著項目的 title 跟是否已經達成的狀態。
const baseState = [
{
title: "Learn TypeScript",
done: true,
},
{
title: "Try Immer",
done: false,
},
];
function App() {
const [state, setState] = useState(baseState);
return (
<div>
<h1>Immer</h1>
<ul>
{state.map(({ title, done }) => (
<li key={title}>
<p>title:{title}</p>
<p>status: {done ? "done" : "idle"}</p>
</li>
))}
</ul>
</div>
);
}
在觸發某個事件的時候,需要新增一個項目並且把其中一個既有項目的達成狀態更改為 true。
function App() {
const [state, setState] = useState(baseState);
function handleState() {
const nextState = state.slice(); // 透過 slice 進行第一層淺拷貝
// 透過展開運算符更新內部物件。
nextState[1] = {
...nextState[1],
done: true,
};
// 在後面新增物件
nextState.push({ title: "Tweet about it", done: false });
// 更新 state
setState(nextState);
}
return (
<div>
<h1>Immer</h1>
<button onClick={handleState}>change</button>
<ul>
{state.map(({ title, done }) => (
<li key={title}>
<p>title:{title}</p>
<p>status: {done ? "done" : "idle"}</p>
</li>
))}
</ul>
</div>
);
}
因為我的 state 是一個 array,所以在進行 setState 要特別注意是不是回傳一個新的 array,以及裡面有修改到的東西是不是回傳一個新的 array 或 object。
為了確保我的 todo list 是 immutable 的,必須要像上面一樣經過非常多的步驟,來一個個的更新 state 裡面的 array 或是 object。
雖然在使用 slice()
進行淺拷貝的時候就可以確定元件會進行 re-render 了,即使裡面的物件沒有透過 ...
來進行處理也沒關係,但是如果我把 todo 的資料透過 props 傳到子元件時,然後子元件又被 memo 進行處理的時候就有可能發生沒有 re-render 的情形。
type Props = { data: { title: string; done: boolean } };
const Item = memo(function Item({ data }: Props) {
return (
<>
<p>title:{data.title}</p>
<p>status: {data.done ? "done" : "idle"}</p>
</>
);
});
function App() {
const [state, setState] = useState(baseState);
function handleState() {
const nextState = state.slice(); // 透過 slice 進行第一層淺拷貝
// 直接修改 nextState[1] 的 done
nextState[1].done = true;
// 在後面新增物件
nextState.push({ title: "Tweet about it", done: false });
// 更新 state
setState(nextState);
}
return (
<div>
<h1>Immer</h1>
<button onClick={handleState}>change</button>
<ul>
{state.map((data) => (
<Item key={data.title} data={data} />
))}
</ul>
</div>
);
}
在這裡我把 todo list 的整份資料都傳遞到子元件,並且在 handleState 裡面透過 mutate 的方式直接修改 todo 裡面 index 為 1 的狀態。
會發現第三項 Tweet about it 有出現在畫面上,代表狀態有成功更新觸發 re-render,但是第二項 Try Immer 的 status 並沒有更新,因為當我們用 mutable 的方法更新資料時 react 用 object.is()
在比較時會認為是相同的資料,所以就不會對 <Item />
進行 re-render,導致畫面顯示的結果是錯誤的。
我們可以使用 immer 透過簡單的語法來協助我們做到 immutable 的狀態管理。
npm install immer
oryarn add immer
const nextState = produce(baseState, recipe: (draftState) => void): nextState
使用 immer 時絕大多數的情況下只會使用 immer 所提供的 produce function,produce 接收兩個參數 baseState
跟 recipe
。
baseState
: 一個你希望它保持 immutable 的東西,通常是一個較為複雜的 array 或是 object。
recipe
: 一個 function,這個 function 會接收到一個 draftState 參數,我們要把這個 draftState 當作我們的 baseState 來使用,當我們透過 mutate 的方式來改變這個 draftState 的時候,immer 會記錄下我們所有的改動,並且在最後回傳一個新的 value 給我們,而不改變本來的 baseState
。
講完語法,就來看看 immer 要怎麼使用吧。
import { useState } from "react";
import { produce } from "immer";
const baseState = [
{
title: "Learn TypeScript",
done: true,
},
{
title: "Try Immer",
done: false,
},
];
function App() {
const [state, setState] = useState(baseState);
function handleState() {
const nextState = produce(state, (draft) => {
draft[1].done = true;
draft.push({ title: "Tweet about it", done: false });
});
console.log("state", Object.is(state, nextState));
console.log("state[0]", Object.is(state[0], nextState[0]));
console.log("state[1]", Object.is(state[1], nextState[1]));
setState(nextState);
}
return (
<div>
<h1>Immer</h1>
<button onClick={handleState}>change</button>
<ul>
{state.map(({ title, done }) => (
<li key={title}>
<p>title:{title}</p>
<p>status: {done ? "done" : "idle"}</p>
</li>
))}
</ul>
</div>
);
在 handleState 裡面我還另外多下了三個 console.log 用來觀察新舊狀態的結果是否有不同。
注意到了嗎?被新增項目的的最外層 array
以及被修改了屬性的 state[1]
的 Object.is
的比較結果是 false
,但是沒有被修改到的 state[0]
的比較結果為 true
,immer 會把我們對 draftState 的操作記錄下來,盡量的減少影響的範圍,只對需要的 object 的做建立的動作,最後回傳一個全新的物件給我們,而且不會對我們傳進去的 baseState 做任何的修改來做到 immutable 的狀態管理。
immer 真的非常好用,用過之後真的有點回不去的感覺,當狀態的層數較多,稍微複雜的時候就會想要使用 immer 來協助管理。
下一篇簡單介紹 SSR 跟 next.js
如果內容有誤再麻煩大家指教,我會盡快修改。
這個系列的文章會同步更新在我個人的 Medium,歡迎大家來看看 👋👋👋
Medium