createSlice
一个接收初始状态、reducer 函数对象和 "slice 名称" 的函数,它可以自动生成与 reducer 和状态相对应的 action creators 和 action types。
这个 API 是编写 Redux 逻辑的标准方法。
在内部,它使用 createAction
和 createReducer
,因此你也可以使用 Immer 来编写 "可变的" 不可变更新:
import { createSlice } from '@reduxjs/toolkit'
import type { PayloadAction } from '@reduxjs/toolkit'
interface CounterState {
value: number
}
const initialState = { value: 0 } satisfies CounterState as CounterState
const counterSlice = createSlice({
name: 'counter',
initialState,
reducers: {
increment(state) {
state.value++
},
decrement(state) {
state.value--
},
incrementByAmount(state, action: PayloadAction<number>) {
state.value += action.payload
},
},
})
export const { increment, decrement, incrementByAmount } = counterSlice.actions
export default counterSlice.reducer
参数
createSlice
接收一个包含以下选项的配置对象参数:
function createSlice({
// 名称,用于 action types
name: string,
// reducer 的初始状态
initialState: State,
// "case reducer" 对象。键名将用于生成 actions。
reducers: Record<string, ReducerFunction | ReducerAndPrepareObject>,
// 一个用于添加更多 reducer 的 "builder callback" 函数
extraReducers?: (builder: ActionReducerMapBuilder<State>) => void,
// slice reducer 位置的偏好设置,由 `combineSlices` 和 `slice.selectors` 使用。默认为 `name`。
reducerPath?: string,
// selector 对象,第一个参数接收 slice 的 state。
selectors?: Record<string, (sliceState: State, ...args: any[]) => any>,
})
initialState
该 state 分片的初始状态值。
这也可以是一个"惰性初始化"函数,调用时应返回一个初始状态值。当 reducer 的 state 参数为 undefined
时会使用这个函数,主要用于从 localStorage
读取初始状态等场景。
name
该状态分片的字符串名称。生成的 action type 常量会使用这个作为前缀。
reducers
包含 Redux "case reducer" 函数的对象(用于处理特定 action type 的函数,相当于 switch 语句中的一个 case)。
对象中的键将用于生成字符串 action type 常量,当这些 action 被分发时,它们会显示在 Redux DevTools Extension 中。另外,如果应用程序的其他部分恰好分发了具有完全相同 type 字符串的 action,相应的 reducer 也会运行。因此,你应该为这些函数起描述性的名称。
这个对象将被传递给 createReducer
,所以这些 reducers 可以安全地"修改"它们接收的 state。
import { createSlice } from '@reduxjs/toolkit'
const counterSlice = createSlice({
name: 'counter',
initialState: 0,
reducers: {
increment: (state) => state + 1,
},
})
// 将处理 action type 'counter/increment'
自定义生成的 Action Creators
如果你需要通过 prepare callback
来自定义 action creator 的 payload 值,reducers
参数对象中相应字段的值应该是一个对象而不是函数。这个对象必须包含两个属性:reducer
和 prepare
。reducer
字段的值应该是 case reducer 函数,而 prepare
字段的值应该是 prepare callback 函数:
import { createSlice, nanoid } from '@reduxjs/toolkit'
import type { PayloadAction } from '@reduxjs/toolkit'
interface Item {
id: string
text: string
}
const todosSlice = createSlice({
name: 'todos',
initialState: [] as Item[],
reducers: {
addTodo: {
reducer: (state, action: PayloadAction<Item>) => {
state.push(action.payload)
},
prepare: (text: string) => {
const id = nanoid()
return { payload: { id, text } }
},
},
},
})
reducers
的 "creator callback" 表示法
或者,reducers
字段可以是一个接收 "create" 对象的回调。
这样做的主要好处是你可以在 slice 中创建 async thunks(尽管出于 bundle 大小的原因,你需要 进行一些设置)。对于准备好的 reducers,类型也稍微简化了一些。
import { createSlice, nanoid } from '@reduxjs/toolkit'
interface Item {
id: string
text: string
}
interface TodoState {
loading: boolean
todos: Item[]
}
const todosSlice = createSlice({
name: 'todos',
initialState: {
loading: false,
todos: [],
} satisfies TodoState as TodoState,
reducers: (create) => ({
deleteTodo: create.reducer<number>((state, action) => {
state.todos.splice(action.payload, 1)
}),
addTodo: create.preparedReducer(
(text: string) => {
const id = nanoid()
return { payload: { id, text } }
},
// action type is inferred from prepare callback
(state, action) => {
state.todos.push(action.payload)
},
),
fetchTodo: create.asyncThunk(
async (id: string, thunkApi) => {
const res = await fetch(`myApi/todos?id=${id}`)
return (await res.json()) as Item
},
{
pending: (state) => {
state.loading = true
},
rejected: (state, action) => {
state.loading = false
},
fulfilled: (state, action) => {
state.loading = false
state.todos.push(action.payload)
},
},
),
}),
})
export const { addTodo, deleteTodo, fetchTodo } = todosSlice.actions
创建方法
create.reducer
一个标准的 slice case reducer。
参数
- reducer 要使用的 slice case reducer。
create.reducer<Todo>((state, action) => {
state.todos.push(action.payload)
})
create.preparedReducer
一个 prepared reducer,用于自定义 action creator。
参数
- prepareAction
prepare callback
。 - reducer 要使用的 slice case reducer。
传递给 case reducer 的 action 将从 prepare callback 的返回值推断。
create.preparedReducer(
(text: string) => {
const id = nanoid()
return { payload: { id, text } }
},
(state, action) => {
state.todos.push(action.payload)
},
)
create.asyncThunk
创建一个 async thunk 而不是 action creator。
为了避免默认情况下将 createAsyncThunk
引入 createSlice
的 bundle 大小,需要进行一些额外的设置来使用 create.asyncThunk
。
从 RTK 导出的 createSlice
版本将抛出错误,如果调用 create.asyncThunk
。
相反,导入 buildCreateSlice
和 asyncThunkCreator
,并创建你自己的 createSlice
版本:
import { buildCreateSlice, asyncThunkCreator } from '@reduxjs/toolkit'
export const createAppSlice = buildCreateSlice({
creators: { asyncThunk: asyncThunkCreator },
})
然后根据需要导入这个 createAppSlice
,而不是从 RTK 导出的版本。
参数
- payloadCreator thunk payload creator。
- config 配置对象。(可选)
配置对象可以包含每个 生命周期 actions(pending
、fulfilled
和 rejected
)的 case reducers,以及一个 settled
reducer,它将在 fulfilled 和 rejected actions 之后运行(注意,这将在任何提供的 fulfilled
/rejected
reducers 之后运行。概念上可以将其视为 finally
块)。
每个 case reducer 将附加到 slice 的 caseReducers
对象,例如 slice.caseReducers.fetchTodo.fulfilled
。
配置对象还可以包含 options
。
create.asyncThunk(
async (id: string, thunkApi) => {
const res = await fetch(`myApi/todos?id=${id}`)
return (await res.json()) as Item
},
{
pending: (state) => {
state.loading = true
},
rejected: (state, action) => {
state.error = action.payload ?? action.error
},
fulfilled: (state, action) => {
state.todos.push(action.payload)
},
settled: (state, action) => {
state.loading = false
}
options: {
idGenerator: uuid,
},
}
)
create.asyncThunk
的类型与 createAsyncThunk
的类型相同,但有一个关键区别。
state
和/或 dispatch
的类型 不能 作为 ThunkApiConfig
的一部分提供,因为这会导致循环类型。
相反,需要在需要时断言类型 - getState() as RootState
。你也可以为 payload 函数包含一个显式的返回类型,以打破循环类型推断。
create.asyncThunk<Todo, string, { rejectValue: { error: string } }>(
// 可能需要包含一个显式的返回类型
async (id: string, thunkApi): Promise<Todo> => {
// 手动转换 `getState` 和 `dispatch` 的类型
const state = thunkApi.getState() as RootState
const dispatch = thunkApi.dispatch as AppDispatch
try {
const todo = await fetchTodo()
return todo
} catch (e) {
throw thunkApi.rejectWithValue({
error: 'Oh no!',
})
}
},
)
对于常见的 thunk API 配置选项,提供了一个 withTypes
helper:
reducers: (create) => {
const createAThunk = create.asyncThunk.withTypes<{
rejectValue: { error: string }
}>()
return {
fetchTodo: createAThunk<Todo, string>(async (id, thunkApi) => {
throw thunkApi.rejectWithValue({
error: 'Oh no!',
})
}),
fetchTodos: createAThunk<Todo[], string>(async (id, thunkApi) => {
throw thunkApi.rejectWithValue({
error: 'Oh no, not again!',
})
}),
}
}
extraReducers
从概念上讲,每个 slice reducer "拥有" 它的 state 分片。reducers
中定义的更新逻辑与基于这些生成的 action types 之间也有自然的对应关系。
然而,很多时候 Redux slice 也需要响应在应用程序其他地方定义的 action types 来更新它自己的 state(例如,当分发 "user logged out" action 时清除许多不同类型的数据)。这可以包括由另一个 createSlice
调用定义的 action types、由 createAsyncThunk
生成的 actions、RTK Query 端点匹配器或任何其他 action。此外,Redux 的一个关键概念是许多 slice reducers 可以独立响应相同的 action type。
extraReducers
允许 createSlice
响应并更新它自己的 state,以响应其他 action types。
与 reducers
字段一样,extraReducers
中的每个 case reducer 都 被 Immer 包装,可以使用 "mutating" 语法安全地更新 state。
然而,与 reducers
字段不同,extraReducers
中的每个 case reducer 都不会生成新的 action type 或 action creator。
如果 reducers
和 extraReducers
中的两个字段恰好具有相同的 action type 字符串,将使用 reducers
中的函数来处理该 action type。
extraReducers
的 "builder callback" 表示法
类似于 createReducer
,extraReducers
字段使用 "builder callback" 表示法来定义特定 action types 的处理程序,匹配一系列 actions 或处理默认情况。这在概念上类似于 switch 语句,但具有更好的 TS 支持,因为它可以从提供的 action creator 推断 action type。它对于处理由 createAction
和 createAsyncThunk
生成的 actions 特别有用。
import { createAction, createSlice, Action } from '@reduxjs/toolkit'
const incrementBy = createAction<number>('incrementBy')
const decrement = createAction('decrement')
interface RejectedAction extends Action {
error: Error
}
function isRejectedAction(action: Action): action is RejectedAction {
return action.type.endsWith('rejected')
}
createSlice({
name: 'counter',
initialState: 0,
reducers: {},
extraReducers: builder => {
builder
.addCase(incrementBy, (state, action) => {
// 如果使用TS,这里可以正确地推断出 action
})
// 你可以链式调用,或者每次都有单独的 `builder.addCase()` 行
.addCase(decrement, (state, action) => {})
// 你可以匹配一系列的 action 类型
.addMatcher(
isRejectedAction,
// 由于 isRejectedAction 被定义为类型保护,`action` 将被推断为 RejectedAction
(state, action) => {}
)
// 如果没有其他处理器匹配,可以提供一个默认情况
.addDefaultCase((state, action) => {})
}
})
有关如何使用 builder.addCase
、builder.addMatcher
和 builder.addDefaultCase
的详细信息,请参阅 createReducer
参考的 "Builder Callback Notation" 部分
reducerPath
指示 slice 应位于的位置偏好。默认为 name
。
这由 combineSlices
和默认生成的 slice.selectors
使用。
selectors
一组 selectors,第一个参数接收 slice 的 state,其他参数可选。
每个 selector 将在结果的 selectors
对象中有一个对应的键。
定义使用其他 selectors 的 selectors 是相当常见的。这在 slice selectors 中仍然是可能的,但定义没有返回类型的 selector 可能会导致循环类型推断问题:
const counterSlice = createSlice({
name: 'counter',
initialState: { value: 0 },
reducers: {},
selectors: {
selectValue: (state) => state.value,
// 这会创建一个循环,因为它从我们在这里创建的对象中推断类型
selectTimes: (state, times = 1) =>
counterSlice.getSelectors().selectValue(state) * times,
},
})
通过为 selector 提供显式的返回类型可以解决这个循环问题:
const counterSlice = createSlice({
name: 'counter',
initialState: { value: 0 },
reducers: {},
selectors: {
selectValue: (state) => state.value,
// 显式返回类型意味着循环被打破
selectTimes: (state, times = 1): number =>
counterSlice.getSelectors().selectValue(state) * times,
},
})
当使用 slice 的 asyncThunk
creator 时,也可能遇到这个限制。
同样,通过在链中的某个地方显式提供类型并打破循环来解决问题。
const counterSlice = createSlice({
name: 'counter',
initialState: { value: 0 },
reducers: (create) => ({
getCountData: create.asyncThunk(async (_arg, { getState }) => {
const currentCount = counterSlice.selectors.selectValue(
getState() as RootState,
)
// 这会导致循环类型,但类型注解打破了循环
const result: Response = await fetch('api/' + currentCount)
return result.json()
}),
}),
selectors: {
selectValue: (state) => state.value,
},
})
返回值
createSlice
将返回一个对象,如下所示:
{
name: string,
reducer: ReducerFunction,
actions: Record<string, ActionCreator>,
caseReducers: Record<string, CaseReducer>.
getInitialState: () => State,
reducerPath: string,
selectSlice: Selector;
selectors: Record<string, Selector>,
getSelectors: (selectState: (rootState: RootState) => State) => Record<string, Selector>
injectInto: (injectable: Injectable, config?: InjectConfig & { reducerPath?: string }) => InjectedSlice
}
在 reducers
参数中定义的每个函数将有一个对应的 action creator 使用 createAction
生成,并使用相同的函数名包含在结果的 actions
字段中。
生成的 reducer
函数适合传递给 Redux 的 combineReducers
函数作为 "slice reducer"。
你可能需要考虑解构 action creators 并单独导出它们,以便在更大的代码库中更容易搜索引用。
传递给 reducers
参数的函数可以通过 caseReducers
返回字段访问。这对于测试或直接访问内联创建的 reducers 特别有用。
结果的 getInitialState
函数提供对传递给 slice 的初始状态值的访问。如果提供了惰性状态初始化器,它将被调用并返回一个新的值。
injectInto
创建一个知道它已被注入的 slice 实例 - 请参阅 combineSlices
。
结果对象在概念上类似于 "Redux duck" 代码结构。 你使用的实际代码结构取决于你,但请记住,actions 并不仅限于单个 slice。 reducer 逻辑的任何部分都可以(并且应该!)响应任何分发的 action。
Selectors
Slice selectors 被编写为期望 slice 的 state 作为它们的第一个参数,但 slice 可能位于 store 的根 state 中的任何位置。
因此,有两种获 取最终 selectors 的方法:
selectors
最常见的是,slice 可靠地挂载在其 reducerPath
下。
在此之后,slice 附加了一个 selectSlice
selector,它假定 slice 位于 rootState[slice.reducerPath]
下。
然后 slice.selectors
使用这个 selector 包装每个提供的 selector。
import { createSlice } from '@reduxjs/toolkit'
interface CounterState {
value: number
}
const counterSlice = createSlice({
name: 'counter',
initialState: { value: 0 } satisfies CounterState as CounterState,
reducers: {
// 省略
},
selectors: {
selectValue: (sliceState) => sliceState.value,
},
})
console.log(counterSlice.selectSlice({ counter: { value: 2 } })) // { value: 2 }
const { selectValue } = counterSlice.selectors
console.log(selectValue({ counter: { value: 2 } })) // 2
传递的原始 selector 作为 .unwrapped
附加到包装的 selector。例如:
import { createSlice, createSelector } from '@reduxjs/toolkit'
interface CounterState {
value: number
}
const counterSlice = createSlice({
name: 'counter',
initialState: { value: 0 } satisfies CounterState as CounterState,
reducers: {
// 省略
},
selectors: {
selectDouble: createSelector(
(sliceState: CounterState) => sliceState.value,
(value) => value * 2,
),
},
})
const { selectDouble } = counterSlice.selectors
console.log(selectDouble({ counter: { value: 2 } })) // 4
console.log(selectDouble({ counter: { value: 3 } })) // 6
console.log(selectDouble.unwrapped.recomputations) // 2
getSelectors
slice.getSelectors
被调用时接收一个参数 selectState
回调。这个函数应该接收 store 根 state(或你期望调用结果 selectors 的任何内容)并返回 slice state。
const { selectValue } = counterSlice.getSelectors(
(rootState: RootState) => rootState.aCounter,
)
console.log(selectValue({ aCounter: { value: 2 } })) // 2
如果没有传递 selectState
回调,selectors 将按原样返回 - 期望 slice state 作为它们的第一个参数(与调用 slice.getSelectors(state => state)
相同)。
const { selectValue } = counterSlice.getSelectors()
console.log(selectValue({ value: 2 })) // 2
slice.selectors
对象相当于调用
const { selectValue } = counterSlice.getSelectors(counterSlice.selectSlice)
// 或
const { selectValue } = counterSlice.getSelectors(
(state: RootState) => state[counterSlice.reducerPath],
)
示例
import { createSlice, createAction, configureStore } from '@reduxjs/toolkit'
import type { PayloadAction } from '@reduxjs/toolkit'
import { combineReducers } from 'redux'
const incrementBy = createAction<number>('incrementBy')
const decrementBy = createAction<number>('decrementBy')
const counter = createSlice({
name: 'counter',
initialState: 0 satisfies number as number,
reducers: {
increment: (state) => state + 1,
decrement: (state) => state - 1,
multiply: {
reducer: (state, action: PayloadAction<number>) => state * action.payload,
prepare: (value?: number) => ({ payload: value || 2 }), // 如果 payload 是假值,则回退
},
},
extraReducers: (builder) => {
builder.addCase(incrementBy, (state, action) => {
return state + action.payload
})
builder.addCase(decrementBy, (state, action) => {
return state - action.payload
})
},
})
const user = createSlice({
name: 'user',
initialState: { name: '', age: 20 },
reducers: {
setUserName: (state, action) => {
state.name = action.payload // 使用 immer 可以随意修改 state
},
},
extraReducers: (builder) => {
builder.addCase(counter.actions.increment, (state, action) => {
state.age += 1
})
},
})
const store = configureStore({
reducer: {
counter: counter.reducer,
user: user.reducer,
},
})
store.dispatch(counter.actions.increment())
// -> { counter: 1, user: {name : '', age: 21} }
store.dispatch(counter.actions.increment())
// -> { counter: 2, user: {name: '', age: 22} }
store.dispatch(counter.actions.multiply(3))
// -> { counter: 6, user: {name: '', age: 22} }
store.dispatch(counter.actions.multiply())
// -> { counter: 12, user: {name: '', age: 22} }
console.log(counter.actions.decrement.type)
// -> "counter/decrement"
store.dispatch(user.actions.setUserName('eric'))
// -> { counter: 12, user: { name: 'eric', age: 22} }