createReducer()
概述
一个简化创建 Redux reducer 函数的实用程序。它在内部使用 Immer,通过在你的 reducer 中编写“可变”代码来极大地简化不可变更新逻辑,并支持直接将特定动作类型映射到 case reducer 函数,这些函数将在调度该动作时更新状态。
Redux reducer 通常使用 switch
语句实现,每个处理的动作类型对应一个 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
case 或设置初始状态。
createReducer
助手简化了此类 reducer 的实现。它使用“构建器回调”符号来定义特定动作类型的处理程序,匹配一系列动作,或处理默认情况。这在概念上类似于 switch 语句,但具有更好的 TS 支持。
使用 createReducer
,你的 reducer 将看起来像
- TypeScript
- JavaScript
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
})
})
import { createAction, createReducer } from '@reduxjs/toolkit'
const increment = createAction('counter/increment')
const decrement = createAction('counter/decrement')
const incrementByAmount = createAction('counter/incrementByAmount')
const initialState = { value: 0 }
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
对象作为其参数。该构建器提供addCase
、addMatcher
和addDefaultCase
函数,这些函数可以被调用来定义此 reducer 将处理的操作。
参数
- initialState
State | (() => State)
: 当 reducer 第一次被调用时应该使用的初始状态。这也可以是一个“延迟初始化器”函数,该函数在被调用时应该返回一个初始状态值。当 reducer 被调用时,其状态值为undefined
,它将被使用,这主要用于从localStorage
读取初始状态等情况。 - builderCallback
(builder: Builder) => void
一个回调函数,它接收一个builder对象,通过调用builder.addCase(actionCreatorOrType, reducer)
来定义 case reducer。
示例用法
- TypeScript
- JavaScript
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 is inferred correctly here
state.counter += action.payload
})
// You can chain calls, or have separate `builder.addCase()` lines each time
.addCase(decrement, (state, action) => {
state.counter -= action.payload
})
// You can apply a "matcher function" to incoming actions
.addMatcher(isActionWithNumberPayload, (state, action) => {})
// and provide a default case if no other handlers matched
.addDefaultCase((state, action) => {})
}
)
import { createAction, createReducer } from '@reduxjs/toolkit'
const increment = createAction('increment')
const decrement = createAction('decrement')
function isActionWithNumberPayload(action) {
return typeof action.payload === 'number'
}
const reducer = createReducer(
{
counter: 0,
sumOfNumberPayloads: 0,
unhandledActions: 0,
},
(builder) => {
builder
.addCase(increment, (state, action) => {
// action is inferred correctly here
state.counter += action.payload
})
// You can chain calls, or have separate `builder.addCase()` lines each time
.addCase(decrement, (state, action) => {
state.counter -= action.payload
})
// You can apply a "matcher function" to incoming actions
.addMatcher(isActionWithNumberPayload, (state, action) => {})
// and provide a default case if no other handlers matched
.addDefaultCase((state, action) => {})
}
)
构建器方法
builder.addCase
添加一个 case reducer 来处理单个确切的动作类型。
所有对builder.addCase
的调用必须在任何对builder.addMatcher
或builder.addDefaultCase
的调用之前。
参数
- actionCreator 一个简单的动作类型字符串,或者一个由
createAction
生成的 action creator,它可以用来确定动作类型。 - reducer 实际的 case reducer 函数。
builder.addMatcher
允许您将传入的动作与您自己的过滤器函数进行匹配,而不是仅匹配action.type
属性。
如果多个匹配器 reducer 匹配,它们将按照定义的顺序执行 - 即使 case reducer 已经匹配。所有对builder.addMatcher
的调用必须在任何对builder.addCase
的调用之后,并且在任何对builder.addDefaultCase
的调用之前。
参数
- matcher 一个匹配器函数。在 TypeScript 中,这应该是一个类型谓词函数
- reducer 实际的 case reducer 函数。
- TypeScript
- JavaScript
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 can be defined outside as a type predicate function
.addMatcher(isPendingAction, (state, action) => {
state[action.meta.requestId] = 'pending'
})
.addMatcher(
// matcher can be defined inline as a type predicate function
(action): action is RejectedAction => action.type.endsWith('/rejected'),
(state, action) => {
state[action.meta.requestId] = 'rejected'
}
)
// matcher can just return boolean and the matcher can receive a generic argument
.addMatcher<FulfilledAction>(
(action) => action.type.endsWith('/fulfilled'),
(state, action) => {
state[action.meta.requestId] = 'fulfilled'
}
)
})
import { createAction, createReducer } from '@reduxjs/toolkit'
const initialState = {}
const resetAction = createAction('reset-tracked-loading-state')
function isPendingAction(action) {
return typeof action.type === 'string' && action.type.endsWith('/pending')
}
const reducer = createReducer(initialState, (builder) => {
builder
.addCase(resetAction, () => initialState)
// matcher can be defined outside as a type predicate function
.addMatcher(isPendingAction, (state, action) => {
state[action.meta.requestId] = 'pending'
})
.addMatcher(
// matcher can be defined inline as a type predicate function
(action) => action.type.endsWith('/rejected'),
(state, action) => {
state[action.meta.requestId] = 'rejected'
}
)
// matcher can just return boolean and the matcher can receive a generic argument
.addMatcher(
(action) => action.type.endsWith('/fulfilled'),
(state, action) => {
state[action.meta.requestId] = 'fulfilled'
}
)
})
builder.addDefaultCase
添加一个“默认情况”的 reducer,如果此操作没有执行任何 case reducer 和 matcher reducer,则会执行此 reducer。
参数
- reducer 回退的“默认情况” reducer 函数。
- TypeScript
- JavaScript
import { createReducer } from '@reduxjs/toolkit'
const initialState = { otherActions: 0 }
const reducer = createReducer(initialState, (builder) => {
builder
// .addCase(...)
// .addMatcher(...)
.addDefaultCase((state, action) => {
state.otherActions++
})
})
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
示例用法
- TypeScript
- JavaScript
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 is inferred correctly here
state.counter += action.payload
})
// You can chain calls, or have separate `builder.addCase()` lines each time
.addCase(decrement, (state, action) => {
state.counter -= action.payload
})
// You can apply a "matcher function" to incoming actions
.addMatcher(isActionWithNumberPayload, (state, action) => {})
// and provide a default case if no other handlers matched
.addDefaultCase((state, action) => {})
}
)
import { createAction, createReducer } from '@reduxjs/toolkit'
const increment = createAction('increment')
const decrement = createAction('decrement')
function isActionWithNumberPayload(action) {
return typeof action.payload === 'number'
}
const reducer = createReducer(
{
counter: 0,
sumOfNumberPayloads: 0,
unhandledActions: 0,
},
(builder) => {
builder
.addCase(increment, (state, action) => {
// action is inferred correctly here
state.counter += action.payload
})
// You can chain calls, or have separate `builder.addCase()` lines each time
.addCase(decrement, (state, action) => {
state.counter -= action.payload
})
// You can apply a "matcher function" to incoming actions
.addMatcher(isActionWithNumberPayload, (state, action) => {})
// and provide a default case if no other handlers matched
.addDefaultCase((state, action) => {})
}
)
直接状态变异
Redux 要求 reducer 函数是纯函数,并将状态值视为不可变的。虽然这对使状态更新可预测和可观察至关重要,但有时会使这种更新的实现变得笨拙。考虑以下示例
- TypeScript
- JavaScript
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),
]
})
})
import { createAction, createReducer } from '@reduxjs/toolkit'
const addTodo = createAction('todos/add')
const toggleTodo = createAction('todos/toggle')
const todosReducer = createReducer([], (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 收到一个代理状态,该状态将所有变异转换为等效的复制操作。
- TypeScript
- JavaScript
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) => {
// This push() operation gets translated into the same
// extended-array creation as in the previous example.
const todo = action.payload
state.push(todo)
})
.addCase(toggleTodo, (state, action) => {
// The "mutating" version of this case reducer is much
// more direct than the explicitly pure one.
const index = action.payload
const todo = state[index]
todo.completed = !todo.completed
})
})
import { createAction, createReducer } from '@reduxjs/toolkit'
const addTodo = createAction('todos/add')
const toggleTodo = createAction('todos/toggle')
const todosReducer = createReducer([], (builder) => {
builder
.addCase(addTodo, (state, action) => {
// This push() operation gets translated into the same
// extended-array creation as in the previous example.
const todo = action.payload
state.push(todo)
})
.addCase(toggleTodo, (state, action) => {
// The "mutating" version of this case reducer is much
// more direct than the explicitly pure one.
const index = action.payload
const todo = state[index]
todo.completed = !todo.completed
})
})
编写“变异”reducer 简化了代码。它更短,间接性更少,并且消除了在传播嵌套状态时常见的错误。但是,使用 Immer 会增加一些“魔法”,并且 Immer 在行为方面也有其自身的细微差别。您应该通读 immer 文档中提到的陷阱。最重要的是,您需要确保要么修改 state
参数,要么返回一个新状态,但不能同时进行。例如,如果传递了 toggleTodo
操作,以下 reducer 将抛出异常
- TypeScript
- JavaScript
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]
// This case reducer both mutates the passed-in state...
todo.completed = !todo.completed
// ... and returns a new value. This will throw an
// exception. In this example, the easiest fix is
// to remove the `return` statement.
return [...state.slice(0, index), todo, ...state.slice(index + 1)]
})
})
import { createAction, createReducer } from '@reduxjs/toolkit'
const toggleTodo = createAction('todos/toggle')
const todosReducer = createReducer([], (builder) => {
builder.addCase(toggleTodo, (state, action) => {
const index = action.payload
const todo = state[index]
// This case reducer both mutates the passed-in state...
todo.completed = !todo.completed
// ... and returns a new value. This will throw an
// exception. In this example, the easiest fix is
// to remove the `return` statement.
return [...state.slice(0, index), todo, ...state.slice(index + 1)]
})
})
多个 Case Reducer 执行
最初,createReducer
始终将给定的操作类型与单个 case reducer 匹配,并且对于给定的操作,只有该 case reducer 会执行。
使用操作匹配器会改变这种行为,因为多个匹配器可能会处理单个操作。
对于任何分派的 action,其行为如下:
- 如果存在与 action 类型完全匹配的 case reducer,则相应的 case reducer 将首先执行。
- 任何返回
true
的匹配器将按照其定义的顺序执行。 - 如果提供了默认 case reducer,并且没有 case 或 matcher reducer 运行,则默认 case reducer 将执行。
- 如果没有任何 case 或 matcher reducer 运行,则将返回原始存在的 state 值,保持不变。
执行的 reducer 形成一个管道,每个 reducer 都将接收前一个 reducer 的输出。
- TypeScript
- JavaScript
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' }))
// Returns 7, as the 'increment' case and both matchers all ran in sequence:
// - case 'increment": 0 => 1
// - matcher starts with 'i': 1 => 5
// - matcher ends with 't': 5 => 7
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' }))
// Returns 7, as the 'increment' case and both matchers all ran in sequence:
// - case 'increment": 0 => 1
// - matcher starts with 'i': 1 => 5
// - matcher ends with 't': 5 => 7
记录草稿状态值
在开发过程中,开发人员经常调用 console.log(state)
。但是,浏览器以难以阅读的格式显示代理,这使得基于 Immer 的 state 的控制台日志记录变得困难。
使用 createSlice
或 createReducer
时,可以使用我们从 immer
库 重新导出的 current
实用程序。此实用程序创建当前 Immer Draft
状态值的单独纯副本,然后可以将其记录以正常查看。
- TypeScript
- JavaScript
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))
},
},
})
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))
},
},
})