跳至主要內容

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

此狀態區段的字串名稱。產生的動作類型常數會使用此名稱作為前綴。

reducers

包含 Redux「個案 reducer」函式的物件(用於處理特定動作類型,等同於 switch 中的單一 case 陳述式)。

物件中的金鑰會用於產生字串動作類型常數,而這些常數會在 Redux DevTools Extension 中顯示,當它們被發送時。此外,如果應用程式的任何其他部分碰巧發送動作,且類型字串完全相同,則會執行對應的 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'`

自訂產生的動作建立器

如果您需要透過 準備回呼自訂動作建立器的有效負載值的建立,則 reducers 參數物件的適當欄位的應為物件,而非函式。此物件必須包含兩個屬性:reducerpreparereducer 欄位的應為個案 reducer 函式,而 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 欄位可以是一個接收「建立」物件的回呼。

這樣做最大的好處是,您可以建立 非同步 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

標準區段個案 reducer。

參數

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

create.preparedReducer

一個 已準備 的 reducer,用於自訂動作建立器。

參數

  • prepareAction 準備回呼
  • reducer 要使用的切片案例 reducer。

傳遞給案例 reducer 的動作會從準備回呼的回傳值推斷。

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 需要一些額外的設定。

如果呼叫 create.asyncThunk,RTK 匯出的 createSlice 版本會擲回錯誤。

取而代之,匯入 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

設定物件也可以包含 選項

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 中提供 state 和/或 dispatch 的類型,因為這會導致循環類型。

相反地,需要在需要時聲明類型 - getState() as RootState。您也可以為 payload 函式包含一個明確的回傳類型,以中斷循環類型推論週期。

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 內定義的更新邏輯與根據這些邏輯產生的動作類型之間也有自然的對應關係。

然而,Redux 區塊經常需要根據在應用程式其他地方定義的動作類型來更新其自己的狀態(例如,在「使用者登出」動作被發送時清除許多不同種類的資料)。這可能包括由另一個 createSlice 呼叫、由 createAsyncThunk 產生的動作、RTK Query 端點比對器或任何其他動作所定義的動作類型。此外,Redux 的一個關鍵概念是,許多區塊 reducer 可以獨立回應同一個動作類型。

extraReducers 允許 createSlice 除了它產生的類型之外,回應並根據其他動作類型更新其自己的狀態。

reducers 欄位一樣,extraReducers 中的每個 case reducer 都 包裝在 Immer 中,並可以使用「變異」語法安全地更新其中的狀態

然而,與 reducers 欄位不同,extraReducers 中的每個個別 case reducer 不會 產生新的動作類型或動作建立器。

如果 reducersextraReducers 中的兩個欄位碰巧具有相同的動作類型字串,則會使用 reducers 中的函式來處理該動作類型。

extraReducers「建構函式回呼」表示法

類似於 createReducerextraReducers 欄位使用「建構函式回呼」表示法來定義特定動作類型的處理函式,與一系列動作進行比對,或處理預設情況。這在概念上類似於 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) => {})
},
})

請參閱 createReducer 參考文件中的「建構函式回呼表示法」章節,以了解如何使用 builder.addCasebuilder.addMatcherbuilder.addDefaultCase

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 函式適合傳遞給 Redux combineReducers 函式作為「切片 reducer」。

你可能想要考慮將動作建立函式解構並個別匯出,以便於在較大的程式碼庫中搜尋參考。

傳遞給 reducers 參數的函式可以透過 caseReducers 回傳欄位存取。這對於測試或直接存取內嵌建立的 reducer 特別有用。

結果的函式 getInitialState 提供存取給予切片的初始狀態值。如果提供了延遲狀態初始化函式,它會被呼叫並回傳新的值。

injectInto 建立一個知道自己已被注入的切片實例 - 參閱 combineSlices

備註

結果物件在概念上類似於 "Redux duck" 程式碼結構。實際使用的程式碼結構由您決定,但值得注意的是,動作不只限於單一區塊。還原器邏輯的任何部分都可以(而且應該!)回應任何發送的動作。

選擇器

區塊選擇器寫成預期區塊狀態作為其第一個參數,但區塊可能會位於儲存根狀態內的任何地方。

因此,有兩種取得最終選擇器的方法

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 回呼。此函數應接收儲存根狀態(或您預期用於呼叫結果選擇器的任何內容)並傳回區塊狀態。

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

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

如果沒有傳遞 selectState 回呼,選擇器將按原樣傳回 - 預期區塊狀態作為其第一個參數(與呼叫 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} }