メインコンテンツへスキップ
非公式ベータ版翻訳

このページは PageTurner AI で翻訳されました(ベータ版)。プロジェクト公式の承認はありません。 エラーを見つけましたか? 問題を報告 →

コードスプリッティング

大規模なWebアプリケーションでは、オンデマンドでロードできる複数のJSバンドルにアプリケーションコードを分割することが望ましい場合がよくあります。この戦略は「コードスプリッティング」と呼ばれ、最初にフェッチしなければならないJSペイロードのサイズを減らすことで、アプリケーションのパフォーマンス向上に役立ちます。

Reduxでコードスプリッティングを行うには、ストアにReducerを動的に追加できるようにする必要があります。ただし、Reduxは実際には単一のルートReducer関数しか持ちません。このルートReducerは通常、アプリケーションの初期化時にcombineReducers()または類似の関数を呼び出して生成されます。動的にReducerを追加するには、この関数を再度呼び出してルートReducerを再生成する必要があります。以下では、この問題を解決するためのアプローチと、この機能を提供する2つのライブラリについて説明します。

基本原則

replaceReducerの使用

ReduxストアはreplaceReducer関数を公開しており、これにより現在アクティブなルートReducer関数を新しいルートReducer関数と置き換えます。これを呼び出すと、内部のReducer関数参照が入れ替わり、新しく追加されたスライスReducerが自身を初期化するのに役立つアクションがディスパッチされます:

const newRootReducer = combineReducers({
existingSlice: existingSliceReducer,
newSlice: newSliceReducer
})

store.replaceReducer(newRootReducer)

Reducer注入アプローチ

このセクションでは、Reducerを注入するための手動実装の方法について説明します。

injectReducer関数の定義

アプリケーションのどこからでもstore.replaceReducer()を呼び出せるようにしたい場合が多いでしょう。そのため、既存のすべてのスライスReducerへの参照を保持する再利用可能なinjectReducer()関数を定義し、これをストアインスタンスにアタッチすると便利です。

import { createStore } from 'redux'

// Define the Reducers that will always be present in the application
const staticReducers = {
users: usersReducer,
posts: postsReducer
}

// Configure the store
export default function configureStore(initialState) {
const store = createStore(createReducer(), initialState)

// Add a dictionary to keep track of the registered async reducers
store.asyncReducers = {}

// Create an inject reducer function
// This function adds the async reducer, and creates a new combined reducer
store.injectReducer = (key, asyncReducer) => {
store.asyncReducers[key] = asyncReducer
store.replaceReducer(createReducer(store.asyncReducers))
}

// Return the modified store
return store
}

function createReducer(asyncReducers) {
return combineReducers({
...staticReducers,
...asyncReducers
})
}

これで、新しいReducerをストアに追加するにはstore.injectReducerを呼び出すだけで済みます。

「Reducer Manager」の使用

別のアプローチとして、登録済みのすべてのReducerを追跡しreduce()関数を公開する「Reducer Manager」オブジェクトを作成する方法があります。次の例を考えてみましょう:

export function createReducerManager(initialReducers) {
// Create an object which maps keys to reducers
const reducers = { ...initialReducers }

// Create the initial combinedReducer
let combinedReducer = combineReducers(reducers)

// An array which is used to delete state keys when reducers are removed
let keysToRemove = []

return {
getReducerMap: () => reducers,

// The root reducer function exposed by this object
// This will be passed to the store
reduce: (state, action) => {
// If any reducers have been removed, clean up their state first
if (keysToRemove.length > 0) {
state = { ...state }
for (let key of keysToRemove) {
delete state[key]
}
keysToRemove = []
}

// Delegate to the combined reducer
return combinedReducer(state, action)
},

// Adds a new reducer with the specified key
add: (key, reducer) => {
if (!key || reducers[key]) {
return
}

// Add the reducer to the reducer mapping
reducers[key] = reducer

// Generate a new combined reducer
combinedReducer = combineReducers(reducers)
},

// Removes a reducer with the specified key
remove: key => {
if (!key || !reducers[key]) {
return
}

// Remove it from the reducer mapping
delete reducers[key]

// Add the key to the list of keys to clean up
keysToRemove.push(key)

// Generate a new combined reducer
combinedReducer = combineReducers(reducers)
}
}
}

const staticReducers = {
users: usersReducer,
posts: postsReducer
}

export function configureStore(initialState) {
const reducerManager = createReducerManager(staticReducers)

// Create a store with the root reducer function being the one exposed by the manager.
const store = createStore(reducerManager.reduce, initialState)

// Optional: Put the reducer manager on the store so it is easily accessible
store.reducerManager = reducerManager
}

新しいReducerを追加するには、store.reducerManager.add("asyncState", asyncReducer)を呼び出します。

Reducerを削除するには、store.reducerManager.remove("asyncState")を呼び出します。

Redux Toolkit

Redux Toolkit 2.0には、遅延ロードされるReducerやミドルウェアの一般的な課題(特にTypeScriptサポート)を解決する、コードスプリッティングを簡素化するユーティリティが含まれています。

combineSlices

combineSlicesユーティリティは、Reducerの注入を容易にするために設計されています。また、複数のスライスやReducerを1つのルートReducerに結合できる点でcombineReducersを置き換えるものです。

セットアップ時には、スライスのセットとReducerマップを受け入れ、注入用のメソッドがアタッチされたReducerインスタンスを返します。

注記

combineSlicesの「スライス」は通常createSliceで作成されますが、reducerPathreducerプロパティを持つ「スライス風」オブジェクトであれば何でも使用できます(RTK Query APIインスタンスも互換性があります)。

const withUserReducer = rootReducer.inject({
reducerPath: 'user',
reducer: userReducer
})

const withApiReducer = rootReducer.inject(fooApi)

簡潔さのため、この{ reducerPath, 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
})
注意

命名衝突に注意してください - 後のキーは前のキーを上書きしますが、TypeScriptはこれを検知できません。

スライス注入

スライスを注入するには、combineSlicesから返されたReducerインスタンスでrootReducer.inject(slice)を呼び出します。これにより、スライスがそのreducerPathの下でReducerセットに注入され、そのスライスが注入されたことを認識する型付けがされた結合Reducerインスタンスが返されます。

別の方法として、slice.injectInto(rootReducer)を呼び出すこともできます。このメソッドは注入されたことを認識するスライスインスタンスを返します。両方のメソッドを活用することも可能で、各呼び出しが有用な結果を返すことに加え、combineSlicesは同じリデューサーインスタンスを同じreducerPathに問題なく複数回注入できます。

const withCounterSlice = rootReducer.inject(counterSlice)
const injectedCounterSlice = counterSlice.injectInto(rootReducer)

従来のリデューサー注入とcombineSliceの「メタリデューサー」アプローチの重要な違いは、combineSliceではreplaceReducerが呼び出されない点です。ストアに渡されたリデューサーインスタンスは変更されません。

この設計の結果として、スライスが注入された際にアクションがディスパッチされないため、注入されたスライスの状態は即座にストア状態に反映されません。状態がストアに表示されるのは、何らかのアクションがディスパッチされた後になります。

ただし、セレクターが潜在的にundefinedの状態を考慮する必要を避けるため、combineSlicesには有用なセレクターユーティリティが含まれています。

遅延読み込みスライスの宣言

遅延読み込みされるスライスが推論された状態型に表示されるようにするため、withLazyLoadedSlicesヘルパーが提供されています。これにより後から注入する予定のスライスを宣言でき、状態型でオプショナルとして表示されます。

遅延スライスのインポートを結合リデューサーのファイルに完全に避けるには、モジュール拡張(module augmentation)を使用できます。

// file: reducer.ts
import { combineSlices } from '@reduxjs/toolkit'
import { staticSlice } from './staticSlice'

export interface LazyLoadedSlices {}

export const rootReducer =
combineSlices(staticSlice).withLazyLoadedSlices<LazyLoadedSlices>()

// file: counterSlice.ts
import type { WithSlice } from '@reduxjs/toolkit'
import { createSlice } from '@reduxjs/toolkit'
import { rootReducer } from './reducer'

interface CounterState {
value: number
}

const counterSlice = createSlice({
name: 'counter',
initialState: { value: 0 } as CounterState,
reducers: {
increment: state => void state.value++
},
selectors: {
selectValue: state => state.value
}
})

declare module './reducer' {
// WithSlice utility assumes reducer is under slice.reducerPath
export interface LazyLoadedSlices extends WithSlice<typeof counterSlice> {}

// if it's not, just use a normal key
export interface LazyLoadedSlices {
aCounter: CounterState
}
}

const injectedCounterSlice = counterSlice.injectInto(rootReducer)
const injectedACounterSlice = counterSlice.injectInto(rootReducer, {
reducerPath: 'aCounter'
})

セレクターユーティリティ

injectに加えて、結合リデューサーインスタンスには.selectorメソッドがあり、セレクターをラップするために使用できます。このメソッドは状態オブジェクトをProxyでラップし、注入されたがまだ状態に現れていないリデューサーに対して初期状態を提供します。

injectを呼び出した結果は、セレクターが呼び出されたときに注入されたスライスが常に定義されていることを認識するように型付けされます。

const selectCounterValue = (state: RootState) => state.counter?.value // number | undefined

const withCounterSlice = rootReducer.inject(counterSlice)
const selectCounterValue = withCounterSlice.selector(
state => state.counter.value // number - initial state used if not in store
)

スライスの「注入済み」インスタンスは、スライスセレクターに対しても同じことを行います。渡された状態に存在しない場合、初期状態が提供されます。

const injectedCounterSlice = counterSlice.injectInto(rootReducer)

console.log(counterSlice.selectors.selectValue({})) // runtime error
console.log(injectedCounterSlice.selectors.selectValue({})) // 0

典型的な使用方法

combineSlicesは、スライスが必要になった時点(つまり、読み込まれたコンポーネントからセレクターやアクションがインポートされた時)で注入されるように設計されています。

つまり、典型的な使用方法は以下のような流れになります。

// file: reducer.ts
import { combineSlices } from '@reduxjs/toolkit'
import { staticSlice } from './staticSlice'

export interface LazyLoadedSlices {}

export const rootReducer =
combineSlices(staticSlice).withLazyLoadedSlices<LazyLoadedSlices>()

// file: store.ts
import { configureStore } from '@reduxjs/toolkit'
import { rootReducer } from './reducer'

export const store = configureStore({ reducer: rootReducer })

// file: counterSlice.ts
import type { WithSlice } from '@reduxjs/toolkit'
import { createSlice } from '@reduxjs/toolkit'
import { rootReducer } from './reducer'

const counterSlice = createSlice({
name: 'counter',
initialState: { value: 0 },
reducers: {
increment: state => void state.value++
},
selectors: {
selectValue: state => state.value
}
})

export const { increment } = counterSlice.actions

declare module './reducer' {
export interface LazyLoadedSlices extends WithSlice<typeof counterSlice> {}
}

const injectedCounterSlice = counterSlice.injectInto(rootReducer)

export const { selectValue } = injectedCounterSlice.selectors

// file: Counter.tsx
// by importing from counterSlice we guarantee
// the injection happens before this component is defined
import { increment, selectValue } from './counterSlice'
import { useAppDispatch, useAppSelector } from './hooks'

export default function Counter() {
const dispatch = usAppDispatch()
const value = useAppSelector(selectValue)
return (
<>
<p>{value}</p>
<button onClick={() => dispatch(increment())}>Increment</button>
</>
)
}

// file: App.tsx
import { Provider } from 'react-redux'
import { store } from './store'

// lazily importing the component means that the code
// doesn't actually get pulled in and executed until the component is rendered.
// this means that the inject call only happens once Counter renders
const Counter = React.lazy(() => import('./Counter'))

function App() {
return (
<Provider store={store}>
<Counter />
</Provider>
)
}

createDynamicMiddleware

createDynamicMiddlewareユーティリティは、ストア初期化後にミドルウェアを注入できる「メタミドルウェア」を作成します。

import { createDynamicMiddleware, configureStore } from '@reduxjs/toolkit'
import logger from 'redux-logger'
import reducer from './reducer'

const dynamicMiddleware = createDynamicMiddleware()

const store = configureStore({
reducer,
middleware: getDefaultMiddleware =>
getDefaultMiddleware().concat(dynamicMiddleware.middleware)
})

dynamicMiddleware.addMiddleware(logger)

addMiddleware

addMiddlewareはミドルウェアインスタンスをダイナミックミドルウェアインスタンスが処理するチェーンに追加します。ミドルウェアは注入順に適用され、関数参照によって保存されます(同じミドルウェアは何度注入されても一度だけ適用されます)。

注記

注入されたすべてのミドルウェアは、元のダイナミックミドルウェアインスタンス内に含まれることに注意することが重要です。

import { createDynamicMiddleware, configureStore } from '@reduxjs/toolkit'
import logger from 'redux-logger'
import reducer from './reducer'

const dynamicMiddleware = createDynamicMiddleware()

const store = configureStore({
reducer,
middleware: getDefaultMiddleware =>
getDefaultMiddleware().concat(dynamicMiddleware.middleware)
})

dynamicMiddleware.addMiddleware(logger)

// middleware chain is now [thunk, logger]

順序をより細かく制御したい場合は、複数のインスタンスを使用できます。

import { createDynamicMiddleware, configureStore } from '@reduxjs/toolkit'
import logger from 'redux-logger'
import reducer from './reducer'

const beforeMiddleware = createDynamicMiddleware()
const afterMiddleware = createDynamicMiddleware()

const store = configureStore({
reducer,
middleware: getDefaultMiddleware =>
getDefaultMiddleware()
.prepend(beforeMiddleware.middleware)
.concat(afterMiddleware.middleware)
})

beforeMiddleware.addMiddleware(logger)
afterMiddleware.addMiddleware(logger)

// middleware chain is now [logger, thunk, logger]

withMiddleware

withMiddlewareはアクションクリエイターで、ディスパッチされると含まれるミドルウェアを追加し、追加された拡張機能を含む型付け済みのdispatchバージョンを返します。

const listenerDispatch = store.dispatch(
withMiddleware(listenerMiddleware.middleware)
)

const unsubscribe = listenerDispatch(addListener({ actionCreator, effect }))
// ^? () => void

これは主にReact以外のコンテキストで有用です。Reactを使用する場合は、React連携機能を利用する方がより実用的です。

React連携

@reduxjs/toolkit/reactエントリポイントからインポートすると、動的ミドルウェアのインスタンスに追加メソッドが付属します。

createDispatchWithMiddlewareHook

このメソッドはaddMiddlewareを呼び出し、注入されたミドルウェアを認識する型付けがされたuseDispatchのバージョンを返します。

import { createDynamicMiddleware } from '@reduxjs/toolkit/react'

const dynamicMiddleware = createDynamicMiddleware()

const useListenerDispatch = dynamicMiddleware.createDispatchWithMiddlewareHook(
listenerMiddleware.middleware
)

function Component() {
const dispatch = useListenerDispatch()

useEffect(() => {
const unsubscribe = dispatch(addListener({ actionCreator, effect }))
return unsubscribe
}, [dispatch])
}
注意

ミドルウェアはcreateDispatchWithMiddlewareHookが呼び出されたときに注入されます。useDispatchフックが呼び出されたときではありません。

createDispatchWithMiddlewareHookFactory

このメソッドはReactコンテキストインスタンスを受け取り、そのコンテキストを使用するcreateDispatchWithMiddlewareHookのインスタンスを生成します(カスタムコンテキストの提供参照)。

import { createContext } from 'react'
import { createDynamicMiddleware } from '@reduxjs/toolkit/react'
import type { ReactReduxContextValue } from 'react-redux'

const context = createContext<ReactReduxContextValue | null>(null)

const dynamicMiddleware = createDynamicMiddleware()

const createDispatchWithMiddlewareHook =
dynamicMiddleware.createDispatchWithMiddlewareHookFactory(context)

const useListenerDispatch = createDispatchWithMiddlewareHook(
listenerMiddleware.middleware
)

function Component() {
const dispatch = useListenerDispatch()

useEffect(() => {
const unsubscribe = dispatch(addListener({ actionCreator, effect }))
return unsubscribe
}, [dispatch])
}

サードパーティライブラリとフレームワーク

上記の機能を自動的に追加できる優れた外部ライブラリがいくつかあります: