跳到主要内容

 

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 提供了 addCaseaddMatcheraddDefaultCase 函数,可以调用它们来定义此 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.addMatcherbuilder.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 的状态的控制台记录变得困难。

当使用 createSlicecreateReducer 时,你可以使用我们从 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))
},
},
})