React 的狀態更新要求不可變(immutable)——不能直接修改 state,而是要返回新物件。對於巢狀較深的資料結構,這會帶來大量樣板程式碼。Immer 通過 Proxy 魔法讓你"直接修改"資料,背後幫你生成不可變的新物件。
沒有 Immer 的痛苦
javascript
// 更新深層巢狀狀態
const nextState = {
...state,
users: state.users.map((user) =>
user.id === targetId
? {
...user,
profile: {
...user.profile,
address: {
...user.profile.address,
city: "Beijing",
},
},
}
: user,
),
};
有了 Immer
javascript
import produce from "immer";
const nextState = produce(state, (draft) => {
const user = draft.users.find((u) => u.id === targetId);
user.profile.address.city = "Beijing"; // 直接賦值!
});
produce 接收當前 state 和一個"recipe 函式",recipe 裡的 draft 是 state 的 Proxy,對 draft 的修改會被 Immer 記錄並生成新的不可變物件。原始 state 不會被改變。
在 React useState 中使用
javascript
import produce from "immer";
function TodoList() {
const [todos, setTodos] = useState([
{ id: 1, text: "學習 Immer", done: false },
{ id: 2, text: "寫部落格", done: false },
]);
const toggleTodo = (id) => {
setTodos(
produce((draft) => {
const todo = draft.find((t) => t.id === id);
todo.done = !todo.done;
}),
);
};
const addTodo = (text) => {
setTodos(
produce((draft) => {
draft.push({ id: Date.now(), text, done: false });
}),
);
};
const deleteTodo = (id) => {
setTodos(
produce((draft) => {
const index = draft.findIndex((t) => t.id === id);
draft.splice(index, 1);
}),
);
};
}
在 useReducer 中使用
javascript
const reducer = produce((draft, action) => {
switch (action.type) {
case "INCREMENT":
draft.count++;
break;
case "ADD_ITEM":
draft.items.push(action.payload);
break;
case "UPDATE_ITEM":
const item = draft.items.find((i) => i.id === action.payload.id);
Object.assign(item, action.payload);
break;
}
// 注意:使用 Immer 的 reducer 不需要 return state
});
const [state, dispatch] = useReducer(reducer, initialState);
在 Redux 中使用(配合 RTK)
Redux Toolkit 內建了 Immer,createSlice 裡可以直接"修改"state:
javascript
import { createSlice } from "@reduxjs/toolkit";
const todosSlice = createSlice({
name: "todos",
initialState: [],
reducers: {
addTodo: (state, action) => {
state.push(action.payload); // 直接 push!RTK 內建 Immer
},
toggleTodo: (state, action) => {
const todo = state.find((t) => t.id === action.payload);
todo.done = !todo.done;
},
},
});
效能注意事項
Immer 使用 Proxy,在現代 JS 引擎上效能已經非常好,但有幾點需要注意:
javascript
// ✅ 正常使用
produce(state, (draft) => {
draft.items.push(newItem);
});
// ❌ 不要在 produce 外部使用 draft
let leaked;
produce(state, (draft) => {
leaked = draft; // draft 在 recipe 執行完後會被撤銷
});
leaked.items; // 已經失效!
// ⚠️ 大陣列(萬級)場景下,考慮用 enableMapSet 外掛
import { enableMapSet } from "immer";
enableMapSet(); // 支援 Map 和 Set
總結
Immer 是 React 狀態管理中價效比最高的工具之一:學習成本極低(就是"在 recipe 裡隨便改"),卻能把複雜巢狀狀態的更新程式碼簡化 70% 以上。Redux Toolkit 已內建 Immer,說明這個思路已經被 Redux 官方認可。