createReducer()
概述
这是一个简化创建 Redux reducer 函数的实用工具。它内部使用 Immer 来极大地简化不可变更新逻辑,通过在你的 reducer 中编写"可变"代码,并支持直接将特定的 action 类型映射到 case reducer 函数,当该 action 被调度时,将更新状态。
Redux reducers 通常使用 switch
语句实现,每个处理的 action 类型都有一个 case
。
const initialState = { value: 0 }
function counterReducer(state = initialState, action) {
switch (action.type) {
case 'increment':
return { ...state, value: state.value + 1 }
case 'decrement':
return { ...state, value: state.value - 1 }
case 'incrementByAmount':
return { ...state, value: state.value + action.payload }
default:
return state
}
}
这种方法工作得很好,但是有点模板化且容易出错。例如,很容易忘记 default
情况或设置初始状态。
createReducer
辅助函数简化了这样的 reducer 的实现。它使用"构建器回调"表示法来定义特定 action 类型的处理程序,匹配一系列 action,或处理默认情况。这在概念上类似于 switch 语句,但具有更好的 TS 支持。
使用 createReducer
,你的 reducer 看起来像这样:
import { createAction, createReducer } from '@reduxjs/toolkit'
interface CounterState {
value: number
}
const increment = createAction('counter/increment')
const decrement = createAction('counter/decrement')
const incrementByAmount = createAction<number>('counter/incrementByAmount')
const initialState = { value: 0 } satisfies CounterState as CounterState
const counterReducer = createReducer(initialState, (builder) => {
builder
.addCase(increment, (state, action) => {
state.value++
})
.addCase(decrement, (state, action) => {
state.value--
})
.addCase(incrementByAmount, (state, action) => {
state.value += action.payload
})
})
使用"构建器回调"表示法的用法
此函数接受一个回调,该回调接收一个 builder
对象作为其参数。
该 builder 提供了 addCase
,addMatcher
和 addDefaultCase
函数,可以调用它们来定义此 reducer 将处理哪些动作。
参数
- initialState
State | (() => State)
: 当 reducer 第一次被调用时应使用的初始状态。这也可以是一个"延迟初始化"函数,当被调用时应返回一个初始状态值。每当 reducer 被调用并且其状态值为undefined
时,都会使用这个函数,这主要用于像从localStorage
读取初始状态这样的情况。 - builderCallback
(builder: Builder) => void
一个接收 builder 对象的回调,通过调用builder.addCase(actionCreatorOrType, reducer)
来定义 case reducer。
示例用法
import {
createAction,
createReducer,
UnknownAction,
PayloadAction,
} from "@reduxjs/toolkit";
const increment = createAction<number>("increment");
const decrement = createAction<number>("decrement");
function isActionWithNumberPayload(
action: UnknownAction
): action is PayloadAction<number> {
return typeof action.payload === "number";
}
const reducer = createReducer(
{
counter: 0,
sumOfNumberPayloads: 0,
unhandledActions: 0,
},
(builder) => {
builder
.addCase(increment, (state, action) => {
// 在这里正确推断出 action
state.counter += action.payload;
})
// 你可以链式调用,或者每次都有单独的 `builder.addCase()` 行
.addCase(decrement, (state, action) => {
state.counter -= action.payload;
})
// 你可以对传入的动作应用一个 "匹配函数"
.addMatcher(isActionWithNumberPayload, (state, action) => {})
// 如果没有其他处理器匹配,可以提供一个默认情况
.addDefaultCase((state, action) => {});
}
);
构建器方法
builder.addCase
添加一个用于处理单一精确动作类型的 case reducer。
所有对 builder.addCase
的调用都必须在对 builder.addMatcher
或 builder.addDefaultCase
的任何调用之前。
参数
- actionCreator 可以是一个普通的动作类型字符串,也可以是由
createAction
生成的动作创建器,用于确定动作类型。 - reducer 实际的 case reducer 函数。
builder.addMatcher
允许你将传入的动作与你自己的过滤函数进行匹配,而不仅仅是 action.type
属性。
如果多个 matcher reducers 匹配,它们都将按照定义的顺序执行 - 即使一个 case reducer 已经匹配。
所有对 builder.addMatcher
的调用都必须在对 builder.addCase
的任何调用之后,并在对 builder.addDefaultCase
的任何调用之前。
参数
- matcher 一个 matcher 函数。在 TypeScript 中,这应该是一个 类型谓词 函数
- reducer 实际的 case reducer 函数。
import {
createAction,
createReducer,
AsyncThunk,
UnknownAction,
} from "@reduxjs/toolkit";
type GenericAsyncThunk = AsyncThunk<unknown, unknown, any>;
type PendingAction = ReturnType<GenericAsyncThunk["pending"]>;
type RejectedAction = ReturnType<GenericAsyncThunk["rejected"]>;
type FulfilledAction = ReturnType<GenericAsyncThunk["fulfilled"]>;
const initialState: Record<string, string> = {};
const resetAction = createAction("reset-tracked-loading-state");
function isPendingAction(action: UnknownAction): action is PendingAction {
return typeof action.type === "string" && action.type.endsWith("/pending");
}
const reducer = createReducer(initialState, (builder) => {
builder
.addCase(resetAction, () => initialState)
// matcher 可以在外部定义为类型谓词函数
.addMatcher(isPendingAction, (state, action) => {
state[action.meta.requestId] = "pending";
})
.addMatcher(
// matcher 可以在内部定义为类型谓词函数
(action): action is RejectedAction => action.type.endsWith("/rejected"),
(state, action) => {
state[action.meta.requestId] = "rejected";
}
)
// matcher 可以只返回布尔值,matcher 可以接收一个泛型参数
.addMatcher<FulfilledAction>(
(action) => action.type.endsWith("/fulfilled"),
(state, action) => {
state[action.meta.requestId] = "fulfilled";
}
);
});
builder.addDefaultCase
添加一个"默认 case" reducer,如果没有 case reducer 和 matcher reducer 为此动作执行,那么将执行它。
参数
- reducer 作为回退的 "默认 case" reducer 函数。
import { createReducer } from '@reduxjs/toolkit'
const initialState = { otherActions: 0 }
const reducer = createReducer(initialState, builder => {
builder
// .addCase(...)
// .addMatcher(...)
.addDefaultCase((state, action) => {
state.otherActions++
})
})
返回
生成的 reducer 函数。
reducer 将有一个附加的 getInitialState
函数,当调用时,它将返回初始状态。这可能对测试或与 React 的 useReducer
钩子的使用有用:
const counterReducer = createReducer(0, (builder) => {
builder
.addCase('increment', (state, action) => state + action.payload)
.addCase('decrement', (state, action) => state - action.payload)
})
console.log(counterReducer.getInitialState()) // 0
示例用法
import {
createAction,
createReducer,
UnknownAction,
PayloadAction,
} from "@reduxjs/toolkit";
const increment = createAction<number>("increment");
const decrement = createAction<number>("decrement");
function isActionWithNumberPayload(
action: UnknownAction
): action is PayloadAction<number> {
return typeof action.payload === "number";
}
const reducer = createReducer(
{
counter: 0,
sumOfNumberPayloads: 0,
unhandledActions: 0,
},
(builder) => {
builder
.addCase(increment, (state, action) => {
// 在这里正确推断出 action
state.counter += action.payload;
})
// 你可以链式调用,或者每次都有单独的 `builder.addCase()` 行
.addCase(decrement, (state, action) => {
state.counter -= action.payload;
})
// 你可以对传入的动作应用一个 "匹配函数"
.addMatcher(isActionWithNumberPayload, (state, action) => {})
// 如果没有其他处理器匹配,可以提供一个默认情况
.addDefaultCase((state, action) => {});
}
);
直接状态变更
Redux 要求 reducer 函数是纯的并将状态值视为不可变的。虽然这对于使状态更新可预测和可观察是必要的,但有时可能使这样的更新的实现变得尴尬。考虑以下示例:
import { createAction, createReducer } from '@reduxjs/toolkit'
interface Todo {
text: string
completed: boolean
}
const addTodo = createAction<Todo>('todos/add')
const toggleTodo = createAction<number>('todos/toggle')
const todosReducer = createReducer([] as Todo[], (builder) => {
builder
.addCase(addTodo, (state, action) => {
const todo = action.payload
return [...state, todo]
})
.addCase(toggleTodo, (state, action) => {
const index = action.payload
const todo = state[index]
return [
...state.slice(0, index),
{ ...todo, completed: !todo.completed },
...state.slice(index + 1),
]
})
})
如果你知道 ES6 扩展语法,addTodo
reducer 就很直接。然而,toggleTodo
的代码就不那么直接了,尤其是考虑到它只设置了一个单独的标志。
为了简化,createReducer
使用 immer 让你编写 reducer,就像它们直接修改状态一样。实际上,reducer 接收一个代理状态,将所有变更转换为等效的复制操作。
import { createAction, createReducer } from '@reduxjs/toolkit'
interface Todo {
text: string
completed: boolean
}
const addTodo = createAction<Todo>('todos/add')
const toggleTodo = createAction<number>('todos/toggle')
const todosReducer = createReducer([] as Todo[], (builder) => {
builder
.addCase(addTodo, (state, action) => {
// 这个 push() 操作被转换为与前一个示例中相同的
// 扩展数组创建。
const todo = action.payload
state.push(todo)
})
.addCase(toggleTodo, (state, action) => {
// 这个 case reducer 的"变更"版本比明确纯净的版本
// 更直接。
const index = action.payload
const todo = state[index]
todo.completed = !todo.completed
})
})
编写"变更"的 reducer 简化了代码。它更短,间接性更少,消除了在扩展嵌套状态时常见的错误。然而,使用 Immer 添加了一些"魔法",Immer 在行为上有自己的细微差别。你应该阅读 在 immer 文档中提到的陷阱。最重要的是,你需要确保你要么变更 state
参数,要么返回一个新的状态,但不能两者都做。例如,如果传递了一个 toggleTodo
动作,以下的 reducer 将抛出一个异常:
import { createAction, createReducer } from '@reduxjs/toolkit'
interface Todo {
text: string
completed: boolean
}
const toggleTodo = createAction<number>('todos/toggle')
const todosReducer = createReducer([] as Todo[], (builder) => {
builder.addCase(toggleTodo, (state, action) => {
const index = action.payload
const todo = state[index]
// 这个 case reducer 同时变更了传入的状态...
todo.completed = !todo.completed
// ... 并返回了一个新的值。这将抛出一个
// 异常。在这个示例中,最简单的修复方法是
// 移除 `return` 语句。
return [...state.slice(0, index), todo, ...state.slice(index + 1)]
})
})
多个 Case Reducer 执行
最初,createReducer
总是将给定的动作类型匹配到一个单一的 case reducer,并且只有那一个 case reducer 会为给定的动作执行。
使用动作匹配器改变了这种行为,因为多个匹配器可能处理一个单一的动作。
对于任何调度的动作,行为是:
- 如果动作类型有精确匹配,相应的 case reducer 将首先执行
- 任何返回
true
的匹配器将按照它们定义的顺序执行 - 如果提供了一个默认的 case reducer,并且没有 case 或匹配器 reducer 运行,那么默认的 case reducer 将执行
- 如果没有 case 或匹配器 reducer 运行,原始的现有状态值将不变地返回
执行的 reducer 形成一个管道,每个 reducer 都将接收前一个 reducer 的输出:
import { createReducer } from '@reduxjs/toolkit'
const reducer = createReducer(0, (builder) => {
builder
.addCase('increment', (state) => state + 1)
.addMatcher(
(action) => action.type.startsWith('i'),
(state) => state * 5,
)
.addMatcher(
(action) => action.type.endsWith('t'),
(state) => state + 2,
)
})
console.log(reducer(0, { type: 'increment' }))
// 返回 7,因为 'increment' case 和两个匹配器都按顺序运行了:
// - case 'increment": 0 => 1
// - 匹配器以 'i' 开头: 1 => 5
// - 匹配器以 't' 结尾: 5 => 7
记录草稿状态值
在开发过程中,开发者调用 console.log(state)
是非常常见的。然而,浏览器以难以阅读的格式显示代理,这可能使基于 Immer 的状态的控制台记录变得困难。
当使用 createSlice
或 createReducer
时,你可以使用我们从 immer
库 重新导出的 current
实用工具。这个实用工具创建了当前 Immer Draft
状态值的单独纯复制,然后可以像正常的那样进行记录以供查看。
import { createSlice, current } from '@reduxjs/toolkit'
const slice = createSlice({
name: 'todos',
initialState: [{ id: 1, title: 'Example todo' }],
reducers: {
addTodo: (state, action) => {
console.log('before', current(state))
state.push(action.payload)
console.log('after', current(state))
},
},
})