combineSlices
概要
複数のスライスを単一の Reducer に結合し、初期化後にさらに Reducer を挿入できるようにする関数。
- TypeScript
- JavaScript
// file: slices/index.ts
import { combineSlices } from '@reduxjs/toolkit'
import { api } from './api'
import { userSlice } from './users'
export const rootReducer = combineSlices(api, userSlice)
// file: store.ts
import { configureStore } from '@reduxjs/toolkit'
import { rootReducer } from './slices'
export const store = configureStore({
reducer: rootReducer,
})
// file: slices/index.js
import { combineSlices } from '@reduxjs/toolkit'
import { api } from './api'
import { userSlice } from './users'
export const rootReducer = combineSlices(api, userSlice)
// file: store.js
import { configureStore } from '@reduxjs/toolkit'
import { rootReducer } from './slices'
export const store = configureStore({
reducer: rootReducer,
})
combineSlices
の「スライス」は通常 createSlice
で作成されますが、reducerPath
と reducer
プロパティを持つ「スライスのような」オブジェクトであれば何でも構いません(つまり、RTK Query の API インスタンス も互換性があります)。
const withUserReducer = rootReducer.inject({
reducerPath: 'user',
reducer: userReducer,
})
const withApiReducer = rootReducer.inject(fooApi)
簡潔にするために、このドキュメントでは、この { reducerPath, reducer }
の形状を「スライス」と呼びます。
パラメータ
combineSlices
は、一連のスライスまたは Reducer マップオブジェクトを受け取り、それらを単一の Reducer に結合します。
スライスは reducerPath
にマウントされ、Reducer マップオブジェクトの項目はそれぞれのキーの下にマウントされます。
const rootReducer = combineSlices(counterSlice, baseApi, {
user: userSlice.reducer,
auth: authSlice.reducer,
})
// is like
const rootReducer = combineReducers({
[counterSlice.reducerPath]: counterSlice.reducer,
[baseApi.reducerPath]: baseApi.reducer,
user: userSlice.reducer,
auth: authSlice.reducer,
})
複数のスライス/マップオブジェクトが同じ Reducer パスを持つ場合、引数で後から提供された Reducer が前の Reducer を上書きします。
ただし、型付けではこれを考慮できません。すべての Reducer が一意の場所を目指すようにするのが最善です。
戻り値
combineSlices
は、メソッドがアタッチされた Reducer 関数を返します。
interface CombinedSliceReducer<InitialState, DeclaredState = InitialState>
extends Reducer<DeclaredState, AnyAction, Partial<DeclaredState>> {
withLazyLoadedSlices<LazyLoadedSlices>(): CombinedSliceReducer<
InitialState,
DeclaredState & Partial<LazyLoadedSlices>
>
inject<Slice extends SliceLike>(
slice: Slice,
config?: InjectConfig
): CombinedSliceReducer<InitialState, DeclaredState & WithSlice<Slice>>
selector: {
(selectorFn: Selector, selectState?: SelectFromRootState) => WrappedSelector
original(state: DeclaredState) => InitialState & Partial<DeclaredState>
}
}
withLazyLoadedSlices
ストアから推論される RootState 型を推論する ことをお勧めします。これは Reducer から推論されます。ただし、スライスが遅延ロードされ、推論できない場合は問題が発生する可能性があります。
withLazyLoadedSlices
を使用すると、後で状態に追加されるスライスを宣言できます。これは最終的な状態型に含まれます。
これを管理する 1 つの可能なパターンは、宣言のマージです。
// file: slices/index.ts
import { combineSlices } from '@reduxjs/toolkit'
import { staticSlice } from './static'
export interface LazyLoadedSlices {}
export const rootReducer =
combineSlices(staticSlice).withLazyLoadedSlices<LazyLoadedSlices>()
// keys in LazyLoadedSlices are marked as optional
export type RootState = ReturnType<typeof rootReducer>
// file: slices/lazySlice.ts
import type { WithSlice } from '@reduxjs/toolkit'
import { rootReducer } from '.'
const lazySlice = createSlice({
/* ... */
})
declare module '.' {
export interface LazyLoadedSlices extends WithSlice<typeof lazySlice> {}
}
const injectedReducer = rootReducer.inject(lazySlice)
// and/or
const injectedSlice = lazySlice.injectInto(rootReducer)
上記の例では、reducerPath
の下にマウントされたスライスに WithSlice
ユーティリティ型を使用しています。スライスが別のキーの下にマウントされている場合は、代わりに通常のキーとして宣言できます。
// file: slices/lazySlice.ts
import { rootReducer } from '.'
const lazySlice = createSlice({
/* ... */
})
declare module '.' {
export interface LazyLoadedSlices {
customKey: LazyState
}
}
const injectedReducer = rootReducer.inject({
reducerPath: 'customKey',
reducer: lazySlice.reducer,
})
// and/or
const injectedSlice = lazySlice.injectInto(rootReducer, {
reducerPath: 'customKey',
})
inject
inject
を使用すると、初期化後に Reducer のセットにスライスを追加できます。スライスとオプションの設定が渡されることを想定しており、スライスが含まれた Reducer の更新バージョンを返します。
これは主に、Reducer を遅延ロードする場合に役立ちます。
const reducerWithUser = rootReducer.inject(userSlice)
inject
は元の Reducer の Reducer マップにスライスを追加しますが、アクションはディスパッチしません。
これは、追加された Reducer の状態が、次のアクションがディスパッチされるまでストアに表示されないことを意味します。
Reducer の置き換え
デフォルトでは、Reducer の置き換えは許可されていません。開発モードでは、すでに挿入されている `reducerPath` に新しい Reducer インスタンスを挿入しようとすると、コンソールに警告が記録されます。(同じ Reducer インスタンスが同じ場所に 2 回挿入された場合は警告されません。)
Reducer を新しいインスタンスに置き換えることを許可する場合は、設定オブジェクトの一部として `overrideExisting: true` を明示的に渡す必要があります。
const reducerWithUser = rootReducer.inject(userSlice, {
overrideExisting: true,
})
これは、ホットリロードや、常に `null` を返す関数に置き換えることによる Reducer の「削除」に役立つ場合があります。予測可能な動作のためには、型がパスを占有する予定のすべての可能な Reducer を考慮する必要があることに注意してください。
declare module '.' {
export interface LazyLoadedSlices {
removable: RemovableState | null
}
}
const withInjected = rootReducer.inject(
{ reducerPath: 'removable', reducer: removableReducer },
{ overrideExisting: true },
)
const emptyReducer = () => null
const removeReducer = () =>
rootReducer.inject(
{ reducerPath: 'removable', reducer: emptyReducer },
{ overrideExisting: true },
)
selector
前述のように、アクションがディスパッチされていない場合、挿入された Reducer は状態では未定義のままになる可能性があります。
セレクターを記述する際に、このオプションの可能性のある状態を処理することは不便な場合があります。多くの結果が未定義になる可能性があり、明示的なデフォルトに依存する可能性があるためです。
`selector` を使用すると、これを回避できます。Reducer の状態を `Proxy` でラップすることで、現在挿入されている Reducer が状態では現在 `undefined` の場合に初期状態に評価されるようにします。
declare module '.' {
export interface LazyLoadedSlices extends WithSlice<typeof counterSlice> {}
}
const counterSlice = createSlice({
name: 'counter',
initialState: { value: 0 },
reducers: {
/* ... */
},
})
const withCounter = rootReducer.inject(counterSlice)
const selectCounterValue = (rootState: RootState) => rootState.counter?.value // number | undefined
const wrappedSelectCounterValue = withCounter.selector(
(rootState) => rootState.counter.value, // number
)
console.log(
selectCounterValue({}), // undefined
selectCounterValue({ counter: { value: 2 } }), // 2
wrappedSelectCounterValue({}), // 0
wrappedSelectCounterValue({ counter: { value: 2 } }), // 2
)
`Proxy` は、ランダムに生成されたアクションタイプで Reducer を呼び出すことによって、Reducer の初期状態を取得します。Reducer 内でこれを特別なケースとして処理しようとしないでください。
ネストされた結合 Reducer
ラップされたセレクターは、結合された Reducer によって返された状態を最初の引数として使用することを想定しています。
結合された Reducer がストア状態のさらに深くネストされている場合は、`selectState` コールバックを `selector` の 2 番目の引数として渡します。
interface RootState {
innerCombined: ReturnType<typeof combinedReducer>
}
const selectCounterValue = withCounter.selector(
(combinedState) => combinedState.counter.value,
(rootState: RootState) => rootState.innerCombined,
)
console.log(
selectCounterValue({
innerCombined: {},
}), // 0
selectCounterValue({
innerCombined: {
counter: {
value: 2,
},
},
}), // 2
)
`original`
Immer の使用法 と同様に、`original` 関数が提供され、`Proxy` に提供された元の状態値を取得します。
これは主にデバッグ/検査に役立ちます。`Proxy` インスタンスは読みづらい形式で表示される傾向があるためです。
関数は `selector` 関数のメソッドとしてアタッチされます。
const wrappedSelectCounterValue = withCounter.selector((rootState) => {
console.log(withCounter.selector.original(rootState))
return rootState.counter.value
})
スライスの統合
`injectInto`
`createSlice` によって返されるスライスインスタンスには、`injectInto` メソッドがアタッチされています。これは `combineSlices` から挿入可能な Reducer を受け取り、そのスライスの「挿入された」バージョンを返します。
const injectedCounterSlice = counterSlice.injectInto(rootReducer)
オプションの設定オブジェクトを渡すことができます。これは、`inject` のオプションに加えて `reducerPath` フィールドを使用して、スライスを現在の `reducerPath` プロパティ以外のパスに挿入します。
const aCounterSlice = counterSlice.injectInto(rootReducer, {
reducerPath: 'aCounter',
})
`selectors` / `getSelectors`
`selector` と同様に、「挿入された」スライスインスタンスのセレクターの動作はわずかに異なります。
スライスの状態が渡されたストア状態で未定義の場合、セレクターは代わりにスライスの初期状態で呼び出されます。
`selectors` は、挿入中に `reducerPath` が変更された場合にもその変更を反映します。
console.log(
injectedCounterSlice.selectors.selectValue({}), // 0
injectedCounterSlice.selectors.selectValue({ counter: { value: 2 } }), // 2
aCounterSlice.selectors.selectValue({ aCounter: { value: 2 } }), // 2
)