Reducer
PPG007 ... 2023-10-22 About 3 min
# Reducer
假设有下面这样一个场景,ul 列表中的每个 li 都具有编辑、删除的功能,如果按照 state 的方式可能会写出下面的代码:
// components/TodoList.tsx
import {FC, Fragment, useState} from "react";
interface Todo {
id: string;
content: string;
}
type TodoListProps = {
todos: Array<Todo>;
changeHandler: (task: Todo) => void;
deleteHandler: (id: string) => void;
}
const TodoList: FC<TodoListProps> = ({todos, changeHandler, deleteHandler}) => {
const [editingTodo, setEditingTodo] = useState<Todo>(() => ({id: '', content: ''}));
const isEditing = (todo: Todo) => editingTodo.id === todo.id;
return (
<Fragment>
<ul>
{
todos ? todos.map((todo) => {
return (
<li key={todo.id}>
{
isEditing(todo) ?
<input
value={editingTodo.content}
onChange={(e) => {
setEditingTodo({...editingTodo, content: e.target.value})
}}
/> :
todo.content
}
<button
onClick={
() => {
if (isEditing(todo)) {
changeHandler(editingTodo)
setEditingTodo({id: '', content: ''});
return
}
setEditingTodo(todo)
}}>
{isEditing(todo) ? 'save' : 'edit'}
</button>
<button onClick={() => {
deleteHandler(todo.id)
}}>delete
</button>
</li>
)
}) : undefined
}
</ul>
</Fragment>
)
}
export {TodoList, Todo}
// App.ts
import {FC, Fragment, useState, KeyboardEvent} from "react";
import {Todo, TodoList} from "./components";
const App: FC = () => {
const [todos, setTodos] = useState<Array<Todo>>([]);
const [value, setValue] = useState('');
const changeHandler = (todo: Todo) => {
setTodos(todos.map((item) => {
if (item.id === todo.id) {
return todo;
}
return item
}))
}
const deleteHandler = (id: string) => {
setTodos(todos.filter((todo) => todo.id !== id))
}
const addHandler = (e: KeyboardEvent) => {
if (e.key === 'Enter') {
setTodos([...todos, {id: `${todos.length+1}`, content: value}])
}
}
return (
<Fragment>
<input
value={value}
onChange={(e) => {setValue(e.target.value)}}
onKeyDown={addHandler}
/>
<TodoList todos={todos} changeHandler={changeHandler} deleteHandler={deleteHandler}/>
</Fragment>
)
}
export default App;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
在上面的例子中,App 组件里 setState 被多次调用了,如果以后还要添加其他需求,那么各种事件的处理函数可能会不断增加,为了将这种状态更新逻辑很多的组件的事件处理器集中起来,可以将所有的状态更新逻辑添加到一个外部函数中,即 reducer。通过下面的步骤可以将 useState 转为 useReducer:
- 将设置状态的逻辑修改成 dispatch 的一个 action。
- 编写一个 reducer 函数。
- 在组件中使用 reducer。
reducer 通过事件处理程序 dispatch 一个 action 来表明用户刚刚做了什么,状态更新逻辑保存在其他地方,不再像 state 那样通过事件处理器直接设置 task,而是 dispatch 一个添加、修改、删除任务的 action。
# 使用 Reducer 替换 state
第一步,定义 reducer,替换掉之前的 useState:
interface action extends Todo {
type: 'add' | 'change' | 'delete';
}
const tasksReducer = (prevState: Array<Todo>, action: action): Array<Todo> => {
switch (action.type){
case "add":
return [...prevState, {id: `${prevState.length+1}`, content: action.content}];
case 'change':
return prevState.map((todo) => {
if (todo.id === action.id) {
return {
id: todo.id,
content: action.content,
}
}
return todo
})
case 'delete':
return prevState.filter((todo) => todo.id !== action.id);
}
}
const [todos, dispatch] = useReducer<Reducer<Array<Todo>, action>>(tasksReducer, []);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
useReducer 函数会返回一个数组,第一个元素为一个 state,第二个元素是 dispatch 函数。
第二步,修改之前的事件处理函数:
const changeHandler = (todo: Todo) => {
dispatch({
type: 'change',
...todo,
})
}
const deleteHandler = (id: string) => {
dispatch({
type: 'delete',
id: id,
content: '',
})
}
const addHandler = (e: KeyboardEvent) => {
if (e.key === 'Enter') {
dispatch({
type: 'add',
id: '',
content: value
})
setValue('');
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# Reducer 和 State 的对比
- 通常情况下,使用
useState
时,一开始只需要编写少量的代码,而useReducer
必须提前编写 reducer 函数和需要调度的 actions。当多个事件处理器以相似的方式修改 state 时,useReducer
可以减少代码量。 - 当状态更新逻辑足够简单时,
useState
可读性良好,但是一旦逻辑变得复杂可读性将会下降,useReducer
允许将状态的更新和事件处理器分离以提高可读性。 - 当使用 state 出现问题时,很难发现具体的原因,使用 reducer 时,可以再 reducer 函数中通过日志等形式可以快速排查问题。
- reducer 是一个不依赖组件的纯函数,所以可以对它单独进行测试。
# Reducer 注意事项
- reducer 必须是纯粹的。和状态更新函数类似,reducer 在渲染时运行,它不应该包含异步请求、定时器或者任何副作用,应该以不可变值的方式去更新对象和数组。
- 每个 action 都描述了一个单一的用户交互,即使它会引发数据的多个变化。例如,如果是更新表单数据,应该一次性 dispatch 一个对象而不是分多次 dispatch。