跳至主要内容

createSlice

一个接受初始状态、reducer 函数对象和“切片名称”的函数,自动生成与 reducer 和状态相对应的 action creator 和 action 类型。

此 API 是编写 Redux 逻辑的标准方法。

在内部,它使用 createActioncreateReducer,因此您也可以使用 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({
// A name, used in action types
name: string,
// The initial state for the reducer
initialState: State,
// An object of "case reducers". Key names will be used to generate actions.
reducers: Record<string, ReducerFunction | ReducerAndPrepareObject>,
// A "builder callback" function used to add more reducers
extraReducers?: (builder: ActionReducerMapBuilder<State>) => void,
// A preference for the slice reducer's location, used by `combineSlices` and `slice.selectors`. Defaults to `name`.
reducerPath?: string,
// An object of selectors, which receive the slice's state as their first parameter.
selectors?: Record<string, (sliceState: State, ...args: any[]) => any>,
})

initialState

该状态切片的初始状态值。

这也可以是一个“延迟初始化”函数,该函数在被调用时应该返回一个初始状态值。当 reducer 被调用时,如果其状态值为 undefined,则会使用此函数,这主要用于从 localStorage 读取初始状态等情况。

name

该状态切片的字符串名称。生成的 action 类型常量将使用此名称作为前缀。

reducers

包含 Redux “case reducer” 函数的对象(旨在处理特定 action 类型的函数,相当于 switch 语句中的单个 case 语句)。

对象中的键将用于生成字符串 action 类型常量,这些常量在调度时会在 Redux DevTools Extension 中显示。此外,如果应用程序的任何其他部分恰好调度了具有完全相同类型字符串的 action,则相应的 reducer 将被执行。因此,您应该为函数提供描述性的名称。

此对象将传递给 createReducer,因此 reducer 可以安全地“修改”它们所接收的状态。

import { createSlice } from '@reduxjs/toolkit'

const counterSlice = createSlice({
name: 'counter',
initialState: 0,
reducers: {
increment: (state) => state + 1,
},
})
// Will handle the action type `'counter/increment'`

自定义生成的 action 创建器

如果您需要通过 prepare 回调 自定义 action 创建器的有效负载值的创建,则 reducers 参数对象的相应字段的值应该是一个对象,而不是一个函数。此对象必须包含两个属性:reducerpreparereducer 字段的值应该是 case reducer 函数,而 prepare 字段的值应该是 prepare 回调函数。

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 的“创建器回调”表示法

或者,reducers 字段可以是一个回调函数,该函数接收一个“create”对象。

这样做的主要好处是,您可以将 异步 thunk 作为切片的一部分创建(尽管出于捆绑大小的原因,您 需要为此进行一些设置)。对于准备好的 reducer,类型也略微简化。

reducer 的创建器回调
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

标准切片 case reducer。

参数

  • reducer 要使用的切片案例 reducer。
create.reducer<Todo>((state, action) => {
state.todos.push(action.payload)
})

create.preparedReducer

一个 已准备好的 reducer,用于自定义操作创建器。

参数

  • prepareAction prepare 回调
  • reducer 要使用的切片案例 reducer。

传递给案例 reducer 的操作将从 prepare 回调的返回值推断出来。

create.preparedReducer(
(text: string) => {
const id = nanoid()
return { payload: { id, text } }
},
(state, action) => {
state.todos.push(action.payload)
},
)

create.asyncThunk

创建一个异步 thunk,而不是操作创建器。

设置

为了避免默认情况下将 createAsyncThunk 拉入 createSlice 的捆绑包大小,使用 create.asyncThunk 需要一些额外的设置。

从 RTK 导出的 createSlice 版本如果调用 create.asyncThunk 将抛出错误。

相反,导入 buildCreateSliceasyncThunkCreator,并创建你自己的 createSlice 版本

import { buildCreateSlice, asyncThunkCreator } from '@reduxjs/toolkit'

export const createAppSlice = buildCreateSlice({
creators: { asyncThunk: asyncThunkCreator },
})

然后根据需要导入此 createAppSlice,而不是从 RTK 导出的版本。

参数

配置对象可以包含每个 生命周期操作pendingfulfilledrejected)的案例 reducer,以及一个 settled reducer,它将针对已完成和拒绝的操作运行(注意,这将在任何提供的 fulfilled/rejected reducer 之后运行。从概念上讲,它可以被认为类似于 finally 块)。

每个案例 reducer 将附加到切片的 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 的类型相同,但有一个关键区别。

不能在 ThunkApiConfig 中提供 statedispatch 的类型,因为这会导致循环类型。

相反,需要在需要时断言类型 - getState() as RootState。您也可以为负载函数包含显式返回类型,以打破循环类型推断周期。

create.asyncThunk<Todo, string, { rejectValue: { error: string } }>(
// may need to include an explicit return type
async (id: string, thunkApi): Promise<Todo> => {
// Cast types for `getState` and `dispatch` manually
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 帮助程序

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

从概念上讲,每个切片 reducer “拥有”其状态的一部分。在 reducers 中定义的更新逻辑与基于这些逻辑生成的 action 类型之间也存在自然对应关系。

但是,Redux 切片可能还需要响应在应用程序其他地方定义的 action 类型(例如,在调度“用户注销” action 时清除许多不同类型的数据)来更新其自身状态。这可能包括由另一个 createSlice 调用定义的 action 类型,由 createAsyncThunk 生成的 action,RTK Query 端点匹配器或任何其他 action。此外,Redux 的关键概念之一是,许多切片 reducer 可以独立地响应相同的 action 类型。

extraReducers 允许 createSlice 响应除其生成的类型之外的其他 action 类型并更新其自身状态。

reducers 字段一样,extraReducers 中的每个 case reducer 都 包装在 Immer 中,可以使用“变异”语法安全地更新内部状态

但是,与 reducers 字段不同,extraReducers 中的每个单独的 case reducer 不会生成新的 action 类型或 action 创建者。

如果 reducersextraReducers 中的两个字段最终具有相同的 action 类型字符串,则将使用 reducers 中的函数来处理该 action 类型。

extraReducers “构建器回调”表示法

createReducer 类似,extraReducers 字段使用“构建器回调”符号来定义特定操作类型的处理程序,匹配一系列操作,或处理默认情况。从概念上讲,这类似于 switch 语句,但具有更好的 TS 支持,因为它可以从提供的操作创建器推断操作类型。它在处理由 createActioncreateAsyncThunk 生成的操作时特别有用。

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) => {
// action is inferred correctly here if using TS
})
// You can chain calls, or have separate `builder.addCase()` lines each time
.addCase(decrement, (state, action) => {})
// You can match a range of action types
.addMatcher(
isRejectedAction,
// `action` will be inferred as a RejectedAction due to isRejectedAction being defined as a type guard
(state, action) => {}
)
// and provide a default case if no other handlers matched
.addDefaultCase((state, action) => {})
},
})

有关如何使用 builder.addCasebuilder.addMatcherbuilder.addDefaultCase 的详细信息,请参阅 createReducer 参考中的“构建器回调符号”部分

reducerPath

指示切片应该位于哪个位置的偏好。默认为 name

这由 combineSlices 和默认生成的 slice.selectors 使用。

selectors

一组选择器,它们接收切片状态作为其第一个参数,以及任何其他参数。

每个选择器在生成的 selectors 对象中将有一个对应的键。

循环类型

使用其他选择器的选择器非常常见。这在切片选择器中仍然是可能的,但是定义没有返回值类型的选择器会导致循环类型推断问题

const counterSlice = createSlice({
name: 'counter',
initialState: { value: 0 },
reducers: {},
selectors: {
selectValue: (state) => state.value,
// this creates a cycle, because it's inferring a type from the object we're creating here
selectTimes: (state, times = 1) =>
counterSlice.getSelectors().selectValue(state) * times,
},
})

可以通过为选择器提供显式返回值类型来修复此循环

const counterSlice = createSlice({
name: 'counter',
initialState: { value: 0 },
reducers: {},
selectors: {
selectValue: (state) => state.value,
// explicit return type means cycle is broken
selectTimes: (state, times = 1): number =>
counterSlice.getSelectors().selectValue(state) * times,
},
})

在使用切片的 asyncThunk 创建器时,也可能会遇到此限制。同样,通过在链中某处显式提供类型并打破循环来解决问题。

const counterSlice = createSlice({
name: 'counter',
initialState: { value: 0 },
reducers: (create) => ({
getCountData: create.asyncThunk(async (_arg, { getState }) => {
const currentCount = counterSlice.selectors.selectValue(
getState() as RootState,
)
// this would cause a circular type, but the type annotation breaks the circle
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 参数中定义的每个函数都将使用 createAction 生成一个对应的操作创建器,并使用相同的函数名称包含在结果的 actions 字段中。

生成的 reducer 函数适合作为“切片 reducer”传递给 Redux combineReducers 函数。

您可能希望考虑解构操作创建器并单独导出它们,以便在更大的代码库中更容易搜索引用。

传递给 reducers 参数的函数可以通过 caseReducers 返回字段访问。这对于测试或直接访问内联创建的 reducer 特别有用。

Result 的函数 getInitialState 提供访问传递给切片的初始状态值的途径。如果提供了延迟状态初始化器,它将被调用并返回一个新的值。

injectInto 创建一个切片的实例,它知道自己已经被注入 - 请参阅 combineSlices

注意

结果对象在概念上类似于 "Redux duck" 代码结构。您使用的实际代码结构取决于您,但值得记住的是,操作并不局限于单个切片。任何部分的 reducer 逻辑都可以(也应该!)响应任何分派的 action。

选择器

切片选择器被编写为期望切片的 state 作为其第一个参数,但切片可能位于 store 的根 state 中的任何位置。

因此,有两种方法可以获取最终选择器

selectors

最常见的是,切片可靠地安装在其 reducerPath 下。

在此之后,切片有一个 selectSlice 选择器附加,它假设切片位于 rootState[slice.reducerPath] 下。

然后 slice.selectors 使用此选择器来包装提供的每个选择器。

import { createSlice } from '@reduxjs/toolkit'

interface CounterState {
value: number
}

const counterSlice = createSlice({
name: 'counter',
initialState: { value: 0 } satisfies CounterState as CounterState,
reducers: {
// omitted
},
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
注意

传递的原始选择器附加到包装的选择器作为 .unwrapped。例如

import { createSlice, createSelector } from '@reduxjs/toolkit'

interface CounterState {
value: number
}

const counterSlice = createSlice({
name: 'counter',
initialState: { value: 0 } satisfies CounterState as CounterState,
reducers: {
// omitted
},
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(或您期望使用结果选择器调用的任何内容)并返回切片 state。

const { selectValue } = counterSlice.getSelectors(
(rootState: RootState) => rootState.aCounter,
)

console.log(selectValue({ aCounter: { value: 2 } })) // 2

如果没有传递 selectState 回调,选择器将按原样返回 - 期望切片 state 作为其第一个参数(与调用 slice.getSelectors(state => state) 相同)。

const { selectValue } = counterSlice.getSelectors()

console.log(selectValue({ value: 2 })) // 2
注意

slice.selectors 对象等效于调用

const { selectValue } = counterSlice.getSelectors(counterSlice.selectSlice)
// or
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 }), // fallback if the payload is a falsy value
},
},
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 // mutate the state all you want with immer
},
},
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} }