React + TypeScript: 使用Immer的useImmerReducer钩子编写reducer函数
Immer是一个用于保持数据结构不可变的库。它经常在React官方文档的示例中被使用(如“使用Immer保持对象的不可变”等)。
Immer可以在各种场景中使用不可变的数据结构。例如,React的状态管理就是其中之一。如果对对象的引用没有改变,那么对象本身也不会被修改。此外,复制的成本相对较低。在数据树中未被修改的部分不会被复制,而是与先前的状态共享在内存中。有关Immer的基本用法,请阅读”React + TypeScript: 通过Immer保持状态不可变”。
在本文中我们要介绍的是一个叫做`useImmerReducer`的钩子函数(GitHub上有关`useImmerReducer`的相关内容。请注意,该`README.md`文件的描述可能稍有过时)。使用可变数组方法`push()`和通过数组索引进行赋值,同时可以保持状态的不可变性。
根据React公式文档中的示例进行编写。
这次的话题是将React官方网站上使用useReducer的示例(在“将状态逻辑提取到reducer中”的“步骤3:从组件中使用reducer”中)改写为使用useImmerReducer。最好的例子是下面的示例001,已经进行了修正以适用于TypeScript。
React + TypeScript: 将状态逻辑提取到 Reducer 中的示例001 02
关于这个示例,我在“React + TypeScript: 将状态逻辑分离到Reducer中”里进行了解释,请感兴趣的人参考一下。
使用 ImmerReducer 钩子
useImmerReducer钩子的语法与useReducer相同。
const [state, dispatch] = useImmerReducer(reducer, initialState);
-
- 第1个参数:reducer函数。
-
- 第2个参数:初始状态值。
-
- 返回值:数组。
第1个元素:当前状态。
第2个元素:dispatch函数(将动作发送给reducer)。
然后,将示例应用程序模块src/App.tsx中的导入和调用钩子useReducer替换为useImmerReducer。从事件处理程序中调用dispatch的方式可以保持不变。这是因为传递动作作为dispatch的参数的方式没有改变。
// import { useReducer } from 'react';
import { useImmerReducer } from 'use-immer';
export default function TaskApp() {
// const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);
const [tasks, dispatch] = useImmerReducer(tasksReducer, initialTasks);
}
重写 reducer 函数
在使用TypeScript时,使用ImmerReducer替代Reducer时,对于reducer函数tasksReducer的类型标注会发生变化。函数的第一个参数是封装了状态的draft对象。第二个参数保持不变,仍然是action。第一个参数的名称也被设为draft。
// import type { Reducer } from 'react';
import type { ImmerReducer } from 'use-immer';
// export const tasksReducer: Reducer<TaskType[], ActionType> = (
export const tasksReducer: ImmerReducer<TaskType[], ActionType> = (
// tasks,
draft,
action
) => {
};
可变的draft对象可以通过可变的语法进行修改。由于Immer会保持状态不可变,所以draft状态不会更改。当直接修改draft时,不需要返回值。但是,请不要忘记使用break。
export const tasksReducer: ImmerReducer<TaskType[], ActionType> = (
) => {
switch (action.type) {
case 'added': {
/* return [
...tasks,
{
id: action.id,
text: action.text,
done: false
}
]; */
draft.push({
id: action.id,
text: action.text,
done: false
});
break;
}
case 'changed': {
/* return tasks.map((_task) => {
if (_task.id === action.task.id) {
return action.task;
} else {
return _task;
}
}); */
const index = draft.findIndex((_task) => _task.id === action.task.id);
draft[index] = action.task;
break;
}
case 'deleted': {
// return tasks.filter((_task) => _task.id !== action.id);
return draft.filter((_task) => _task.id !== action.id);
}
};
将useReducer替换为useImmerReducer的结果就是下面的示例002。在reducer函数(tasksReducer)的编写上变得更加简洁。
サンプル002■React + TypeScript:状態のロジックをリデューサに抽出する03
解释:这是一个React + TypeScript的示例,其中介绍了如何将状态逻辑提取到一个reducer中。代码示例可以在上面的链接中找到。
Immer所能做的事情
由于reducer必须是纯粹的,因此不能修改状态。然而,Immer提供的是一种特殊的draft对象,可以安全地进行修改。在内部,Immer会将对draft的更改应用于创建的状态副本上。因此,由useImmerReducer管理的reducer可以直接修改第一个参数(draft),而无需返回状态。
使用TypeScript重新编写和类型化Immer公式的示例。
额外的,我使用TypeScript重新编写了GitHub和Immer官方网站上的两个示例,并对其进行了类型标注。前一个官方示例的语法陈旧,而后一个示例的代码有错误,因此我都进行了修正。
GitHub「useImmerReducer」示例:
GitHub上的「useImmerReducer」示例:
React + TypeScript的サンプル003 useImmerReducer 01的代码可以在codesandbox上找到(链接为https://codesandbox.io/s/react-typescript-useimmerreducer-01-40nmsi)。
Immer公式网站上的「useImmerReducer」演示:
样例004■React + TypeScript:useImmerReducer 02
链接:https://codesandbox.io/s/react-typescript-useimmerreducer-02-qmx5nh