createReducer()
概述
一個簡化建立 Redux reducer 函式的工具程式。它在內部使用 Immer,透過在 reducer 中撰寫「可變」程式碼,大幅簡化不可變更新邏輯,並支援直接將特定 action 類型對應至案例 reducer 函式,這些函式會在該 action 觸發時更新狀態。
Redux reducer 通常使用 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
輔助函式簡化了此類簡化器的實作。它使用「建構函式回呼」符號來定義特定動作類型的處理常式,與一系列動作相符,或處理預設案例。這在概念上類似於 switch 陳述式,但有更好的 TS 支援。
使用 createReducer
,您的簡化器看起來像
- 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
函式,可以呼叫這些函式來定義此簡化器將處理哪些動作。
參數
- initialState
State | (() => State)
:在第一次呼叫簡化器時應使用的初始狀態。這也可能是一個「延遲初始化器」函式,在呼叫時應傳回初始狀態值。這將在簡化器以undefined
作為其狀態值呼叫時使用,並且主要用於從localStorage
讀取初始狀態等情況。 - builderCallback
(builder: Builder) => void
接收一個建構函式物件以透過呼叫builder.addCase(actionCreatorOrType, 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
新增一個案例簡化器來處理單一精確的動作類型。
所有對 builder.addCase
的呼叫都必須在對 builder.addMatcher
或 builder.addDefaultCase
的任何呼叫之前。
參數
- actionCreator 純粹的動作類型字串,或由
createAction
產生的動作建立器,可用於判斷動作類型。 - reducer 實際的案例簡化器函式。
builder.addMatcher
允許您將傳入動作與自己的篩選器函數比對,而不僅限於 action.type
屬性。
如果多個比對器 reducer 相符,則所有比對器 reducer 都會按定義順序執行,即使案例 reducer 已相符。所有對 builder.addMatcher
的呼叫都必須在對 builder.addCase
的任何呼叫之後,以及對 builder.addDefaultCase
的任何呼叫之前。
參數
- matcher 比對器函數。在 TypeScript 中,這應該是 類型謂詞 函數
- 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 和比對器 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)]
})
})
多個案例 reducer 執行
最初,createReducer
總是將給定的動作類型對應到單一案例 reducer,而且只有那個案例 reducer 會針對給定的動作執行。
使用動作比對器會改變該行為,因為多個比對器可能會處理單一動作。
對於任何已發送的動作,行為如下
- 如果動作類型有完全比對,對應的案例 reducer 會先執行
- 任何傳回
true
的比對器會按照定義順序執行 - 如果提供了預設案例 reducer,而且沒有執行任何案例或比對器 reducer,預設案例 reducer 會執行
- 如果沒有執行任何案例或比對器 reducer,現有的原始狀態值會保持不變傳回
執行的 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 為基礎的狀態的記錄器記錄變得困難。
在使用 createSlice
或 createReducer
時,您可以使用 current
工具程式,我們會從 immer
函式庫 重新匯出該工具程式。此工具程式會建立現有 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))
},
},
})