メインコンテンツへスキップ

createSlice

初期状態、リデューサー関数のオブジェクト、および「スライス名」を受け取り、リデューサーと状態に対応するアクション作成者とアクションタイプを自動的に生成します。

この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

Parameters

createSliceは、以下のオプションを含む1つの構成オブジェクトパラメータを受け取ります。

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

この状態スライスの初期状態の値。

これは「遅延初期化」関数でもあり、呼び出されると初期状態の値が返されます。これは、localStorageから初期状態を読み取る場合など、リデューサーがundefinedを状態の値として呼び出されたときにいつでも使用されます。

name

この状態スライスの文字列名。生成されたアクションタイプ定数はこれをプレフィックスとして使用します。

reducers

Redux「ケースリデューサー」関数を含むオブジェクト(特定のアクションタイプを処理することを目的とした関数、スイッチ内の単一ケースステートメントと同じ)。

オブジェクト内のキーは文字列のアクションタイプ定数を生成するために使用され、これらはディスパッチされるとRedux DevTools Extensionに表示されます。また、アプリケーションの他の部分がまったく同じタイプの文字列を持つアクションをディスパッチした場合、対応するリデューサーが実行されます。したがって、その関数にはわかりやすい名前を付ける必要があります。

このオブジェクトはcreateReducerに渡されるため、リデューサーは与えられた状態を安全に「ミューテート」できます。

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引数オブジェクトの適切なフィールドの値は関数ではなくオブジェクトにする必要があります。このオブジェクトには、2つのプロパティ、reducerprepareが含まれている必要があります。 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フィールドは「create」オブジェクトを受け取るコールバックにすることができます。

これの主な利点は、スライスの一部として非同期 thunkを作成できることです(ただし、バンドルサイズの理由から、これには少し設定が必要です)。準備されたリデューサーの場合、型もわずかに簡略化されます。

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

標準のスライスケースリデューサー。

パラメーター

  • reducer:使用するスライスケースリデューサー。
create.reducer<Todo>((state, action) => {
state.todos.push(action.payload)
})

create.preparedReducer

アクションクリエーターをカスタマイズするための準備されたリデューサー。

パラメーター

ケースリデューサーに渡されるアクションは、準備コールバックの戻り値から推測されます。

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

create.asyncThunk

アクションクリエーターではなく非同期 thunk を作成します。

セットアップ

デフォルトでcreate.asyncThunkcreateSliceのバンドルサイズにプルインしないようにするには、create.asyncThunkを使用するために追加の設定が必要です。

RTK からエクスポートされたcreateSliceのバージョンは、create.asyncThunkが呼び出されるとエラーをスローします。

代わりに、buildCreateSliceasyncThunkCreatorをインポートして、独自のバージョンcreateSliceを作成します

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

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

次に、このcreateAppSliceをRTKからエクスポートされたバージョンではなく、必要に応じてインポートします。

パラメーター

構成オブジェクトには、各ライフサイクルフアクション(pending、fulfilled、rejected)のケースリデューサーと、fulfilledアクションとrejectedアクションの両方で実行されるsettledリデューサーを含めることができます(ただし、これらは、指定されたfulfilled/rejectedリデューサーの後に実行されます。概念的には、finallyブロックのようなものと考えることができます)。

各ケースリデューサーは、スライスの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と同じように機能しますが、重要な違いが1つあります。

stateおよび/またはdispatchの型は循環型につながるため、ThunkApiConfigの一部として提供することができません。

その代わり、必要に応じてタイプを宣言する必要があります - 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

各スライスレデューサーは、概念的に状態のスライスを「所有」しています。reducers 内に定義された更新ロジックと、それらに基づいて生成されるアクションタイプとの間に自然な関連性があります。

しかしながら、Redux スライスは、アプリケーションの他の場所(「ユーザーがログアウト」アクションが実行されたときに多数のさまざまなデータのクリアなど)で定義されたアクションタイプに応答して独自のステートを更新する必要がある場合があります。これには、別の createSlice 呼び出しによって定義されたアクションタイプ、createAsyncThunk、RTK クエリエンドポイントマッチャ、またはその他のアクションなどが含まれます。さらに、Redux の重要な概念の 1 つは、多数のスライスレデューサーが同じアクションタイプに独立して応答できることです。

extraReducers により、createSlice は生成したタイプ以外の他のアクションタイプに応答して独自のステートを更新できます。

reducers フィールドと同様に、extraReducers の各ケースレデューサーは Immer でラップされ、「ミューテーション」構文を使用して内部のステートを安全に更新できます

ただし、reducers フィールドとは異なり、extraReducers 内の各個別のケースレデューサーは新しいアクションタイプやアクションクリエイターを生成しません

reducersextraReducers の 2 つのフィールドで、同じアクションタイプの文字列が使用される場合、reducers の関数がそのアクションタイプの処理に使用されます。

extraReducers の「ビルダーコールバック」の表記

createReducer と同様に、extraReducers フィールドは「ビルダーコールバック」表記を使用して特定のアクションタイプのハンドラーを定義し、一連のアクションと照合するか、またはデフォルトケースを処理します。本質的にスイッチステートメントに似ていますが、TS がアクションクリエイターからアクションタイプを推論できるため 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

最初の引数としてスライスのステート、その他のパラメーターを受け取る selector のセットです。

各 selector は、生成される selectors オブジェクトに対応するキーを持ちます。

循環タイプ

他の selector を使用する selector があることは、よくあることです。これはスライス selector でも可能ですが、戻り値のタイプを指定せずに selectorを定義すると、循環参照型推論問題が発生する可能性があります

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,
},
})

この循環は、selector に明示的な戻り値のタイプを提供することで修正できます

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 関数に渡すのに適しています。

より大きなコードベースで参照を検索しやすくするために、アクション作成者をデストラクチャリングして個別にエクスポートすることを検討できます。

reducers パラメータに渡される関数は、caseReducers 戻りフィールドを介してアクセスできます。これは、インラインで作成されたリデューサーのテストまたは直接アクセスに特に役立ちます。

結果の関数の getInitialState は、スライスに与えられた初期状態の値へのアクセスを提供します。遅延状態初期化子が提供された場合、呼び出され、新しい値が返されます。

injectInto は、挿入されたことを認識するスライスのインスタンスを作成します。詳細については、combineSlices を参照してください。

結果オブジェクトと "Redux ダック" コード構造 との概念は似ています。使用する実際のコード構造はお任せしますが、アクションは 1 つのスライスに限定されないことに留意してください。リデューサーロジックのどの部分でも (応答すべきです!) 送信されたアクションに応答できます。

セレクター

スライスセレクターは、スライスの状態を最初の引数として期待するように記述されていますが、スライスはストアのルート状態内の任意の場所に配置できます。

結果として、最終的なセレクターを取得する方法は 2 つあります

selectors

最も一般的なこととして、スライスは reducerPath の下に確実にマウントされます。

これに従って、スライスには、スライスが rootState[slice.reducerPath] の下に位置していると仮定する selectSlice セレクターが添付されます。

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} }