このページは PageTurner AI で翻訳されました(ベータ版)。プロジェクト公式の承認はありません。 エラーを見つけましたか? 問題を報告 →
ストアの設定
"Redux Fundamentals" チュートリアルでは、Todoリストアプリの構築を通じてReduxの基本概念を紹介しました。その中でReduxストアの作成と設定方法についても説明しました。
ここでは、追加機能を組み込むためのストアのカスタマイズ方法を探ります。"Redux Fundamentals" part 5: UI and Reactのソースコードを出発点とします。この段階のチュートリアルソースはGitHubのサンプルアプリリポジトリまたはCodeSandboxのブラウザ環境で閲覧できます。
ストアの作成
まず、ストアを作成した元のindex.jsファイルを見てみましょう:
import React from 'react'
import { render } from 'react-dom'
import { Provider } from 'react-redux'
import { createStore } from 'redux'
import rootReducer from './reducers'
import App from './components/App'
const store = createStore(rootReducer)
render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)
このコードでは、リデューサーをReduxのcreateStore関数に渡してstoreオブジェクトを生成し、そのオブジェクトをreact-reduxのProviderコンポーネントに渡しています。このコンポーネントはコンポーネントツリーの最上位にレンダリングされます。
これにより、アプリ内でreact-reduxのconnectを通じてReduxに接続する際、常にストアがコンポーネントから利用可能な状態になります。
Redux機能の拡張
ほとんどのアプリケーションは、ミドルウェアやストアエンハンサーを追加することでReduxストアの機能を拡張します(注:ミドルウェアは一般的ですが、エンハンサーはあまり使われません)。ミドルウェアはReduxのdispatch関数に追加機能を提供し、エンハンサーはReduxストア自体に追加機能を提供します。
ここでは次の2つのミドルウェアと1つのエンハンサーを追加します:
-
redux-thunkミドルウェア:非同期ディスパッチをシンプルに実現 -
ディスパッチされたアクションと結果の状態を記録するミドルウェア
-
各アクションのリデューサー処理時間を記録するエンハンサー
redux-thunkのインストール
npm install redux-thunk
middleware/logger.js
const logger = store => next => action => {
console.group(action.type)
console.info('dispatching', action)
let result = next(action)
console.log('next state', store.getState())
console.groupEnd()
return result
}
export default logger
enhancers/monitorReducer.js
const round = number => Math.round(number * 100) / 100
const monitorReducerEnhancer =
createStore => (reducer, initialState, enhancer) => {
const monitoredReducer = (state, action) => {
const start = performance.now()
const newState = reducer(state, action)
const end = performance.now()
const diff = round(end - start)
console.log('reducer process time:', diff)
return newState
}
return createStore(monitoredReducer, initialState, enhancer)
}
export default monitorReducerEnhancer
これらを既存のindex.jsに追加しましょう。
-
まず
redux-thunk、自前のloggerMiddlewareとmonitorReducerEnhancer、さらにReduxが提供するapplyMiddlewareとcompose関数をインポートします -
次に
applyMiddlewareを使用し、loggerMiddlewareとthunkミドルウェアをストアのdispatch関数に適用するストアエンハンサーを作成します -
続いて
composeを使用し、新規作成したmiddlewareEnhancerとmonitorReducerEnhancerを単一の関数に合成しますこれは
createStoreに渡せるエンハンサーは1つだけという制約のためです。複数のエンハンサーを使用する場合、この例のようにまず単一の大きなエンハンサーに合成する必要があります。 -
最後に、この新規作成した
composedEnhancers関数をcreateStoreの第3引数として渡します(注:第2引数はストアへの事前状態読み込み用ですが、ここでは無視します)
import React from 'react'
import { render } from 'react-dom'
import { Provider } from 'react-redux'
import { applyMiddleware, createStore, compose } from 'redux'
import { thunk } from 'redux-thunk'
import rootReducer from './reducers'
import loggerMiddleware from './middleware/logger'
import monitorReducerEnhancer from './enhancers/monitorReducer'
import App from './components/App'
const middlewareEnhancer = applyMiddleware(loggerMiddleware, thunk)
const composedEnhancers = compose(middlewareEnhancer, monitorReducerEnhancer)
const store = createStore(rootReducer, undefined, composedEnhancers)
render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)
このアプローチの問題点
このコードは機能しますが、一般的なアプリケーションにとって理想的とは言えません。
ほとんどのアプリでは複数のミドルウェアを使用し、各ミドルウェアには初期設定が必要です。index.jsに追加される余計なコードはすぐに管理を困難にします。ロジックが明確に整理されていないためです。
解決策: configureStore
この問題の解決策は、ストア作成ロジックをカプセル化した新しい configureStore 関数を作成することです。これにより専用ファイルに切り分けられ、拡張性が向上します。
最終的に index.js は次のようになります:
import React from 'react'
import { render } from 'react-dom'
import { Provider } from 'react-redux'
import App from './components/App'
import configureStore from './configureStore'
const store = configureStore()
render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)
ストアの設定に関連するすべてのロジック(リデューサーのインポート、ミドルウェア、エンハンサーの設定など)は専用ファイルで処理されます。
これを実現するため、configureStore 関数は次のように実装します:
import { applyMiddleware, compose, createStore } from 'redux'
import { thunk } from 'redux-thunk'
import monitorReducersEnhancer from './enhancers/monitorReducers'
import loggerMiddleware from './middleware/logger'
import rootReducer from './reducers'
export default function configureStore(preloadedState) {
const middlewares = [loggerMiddleware, thunk]
const middlewareEnhancer = applyMiddleware(...middlewares)
const enhancers = [middlewareEnhancer, monitorReducersEnhancer]
const composedEnhancers = compose(...enhancers)
const store = createStore(rootReducer, preloadedState, composedEnhancers)
return store
}
この関数は前述の手順に沿っていますが、将来の拡張を見越してロジックを分割しています:
-
middlewaresとenhancersは両方とも配列として定義され、それらを処理する関数から分離されています。これにより、異なる条件に基づいて簡単にミドルウェアやエンハンサーを追加できます。
例えば、開発モード時のみ特定のミドルウェアを追加する一般的なパターンは、if文内でミドルウェア配列に追加することで簡単に実現できます:
if (process.env.NODE_ENV === 'development') {
middlewares.push(secretMiddleware)
} -
preloadedState変数は後で追加できるようcreateStoreに渡されます。
この構成は createStore 関数の理解も容易にします。各ステップが明確に分離されているため、何が起きているのかがより明白です。
DevTools拡張の統合
アプリに追加したいもう1つの一般的な機能は redux-devtools-extension の統合です。
この拡張機能はReduxストアを完全に制御するツールスイートです。アクションの検査と再生、異なる時点での状態の確認、ストアへの直接アクションのディスパッチなどが可能です。利用可能な機能の詳細はこちら
統合方法は複数ありますが、最も便利なオプションを使用します。
まずnpm経由でパッケージをインストールします:
npm install --save-dev redux-devtools-extension
次に、redux からインポートしていた compose 関数を削除し、redux-devtools-extension からインポートする新しい composeWithDevTools 関数で置き換えます。
最終的なコードは次のようになります:
import { applyMiddleware, createStore } from 'redux'
import { thunk } from 'redux-thunk'
import { composeWithDevTools } from 'redux-devtools-extension'
import monitorReducersEnhancer from './enhancers/monitorReducers'
import loggerMiddleware from './middleware/logger'
import rootReducer from './reducers'
export default function configureStore(preloadedState) {
const middlewares = [loggerMiddleware, thunk]
const middlewareEnhancer = applyMiddleware(...middlewares)
const enhancers = [middlewareEnhancer, monitorReducersEnhancer]
const composedEnhancers = composeWithDevTools(...enhancers)
const store = createStore(rootReducer, preloadedState, composedEnhancers)
return store
}
これで完了です!
DevTools拡張機能がインストールされたブラウザでアプリにアクセスすると、この強力な新しいツールを使用した探索とデバッグが可能になります。
ホットリローディング
開発プロセスを大幅に直感的にするもう1つの強力なツールがホットリローディングです。これはアプリ全体を再起動せずにコードの一部を置き換える機能です。
例えば、アプリを実行し操作した後、リデューサーの変更を決定した場合を考えてみてください。通常、変更するとアプリが再起動され、Redux状態が初期値にリセットされます。
ホットモジュールリローディングが有効な場合、変更されたリデューサーのみがリロードされ、状態をリセットせずにコードを変更できます。これにより開発プロセスが大幅に高速化されます。
ReduxリデューサーとReactコンポーネントの両方にホットリローディングを追加します。
まず configureStore 関数に追加します:
import { applyMiddleware, compose, createStore } from 'redux'
import { thunk } from 'redux-thunk'
import monitorReducersEnhancer from './enhancers/monitorReducers'
import loggerMiddleware from './middleware/logger'
import rootReducer from './reducers'
export default function configureStore(preloadedState) {
const middlewares = [loggerMiddleware, thunk]
const middlewareEnhancer = applyMiddleware(...middlewares)
const enhancers = [middlewareEnhancer, monitorReducersEnhancer]
const composedEnhancers = compose(...enhancers)
const store = createStore(rootReducer, preloadedState, composedEnhancers)
if (process.env.NODE_ENV !== 'production' && module.hot) {
module.hot.accept('./reducers', () => store.replaceReducer(rootReducer))
}
return store
}
新しいコードは if 文でラップされており、アプリが本番モードではなく、かつ module.hot 機能が利用可能な場合のみ実行されます。
WebpackやParcelなどのバンドラーは、ホットリロードすべきモジュールと変更時の動作を指定する module.hot.accept メソッドをサポートしています。この例では ./reducers モジュールを監視し、変更時に更新された rootReducer を store.replaceReducer メソッドに渡します。
同じパターンを index.js でも使用し、Reactコンポーネントへの変更をホットリロードします:
import React from 'react'
import { render } from 'react-dom'
import { Provider } from 'react-redux'
import App from './components/App'
import configureStore from './configureStore'
const store = configureStore()
const renderApp = () =>
render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)
if (process.env.NODE_ENV !== 'production' && module.hot) {
module.hot.accept('./components/App', renderApp)
}
renderApp()
ここでの唯一の追加変更点は、アプリのレンダリングロジックを新しい renderApp 関数にカプセル化し、アプリを再レンダリングするためにこの関数を呼び出すようにしたことです。
Redux Toolkit を使ったセットアップの簡素化
Redux コアライブラリは意図的に意見を押し付けない設計になっています。ストアの設定方法、状態の内容、リデューサーの構築方法など、すべてを自分で決定できます。
これは柔軟性を提供する点で優れていますが、その柔軟性が常に必要とは限りません。時には、すぐに使える適切なデフォルト動作を持つ、可能な限り簡単な方法で始めたい場合があります。
Redux Toolkit パッケージは、ストア設定を含むいくつかの一般的な Redux のユースケースを簡素化するために設計されています。これがストア設定プロセスをどのように改善するか見てみましょう。
Redux Toolkit には、前述の例で示したような事前構築済みの configureStore 関数が含まれています。
最も簡単な使用方法は、ルートリデューサー関数を渡すだけです:
import { configureStore } from '@reduxjs/toolkit'
import rootReducer from './reducers'
const store = configureStore({
reducer: rootReducer
})
export default store
オブジェクト形式で名前付きパラメーターを受け取るため、何を渡しているかが明確になります。
Redux Toolkit の configureStore はデフォルトで以下を行います:
-
デフォルトのミドルウェアリスト(
redux-thunkを含む)や、状態のミューテーションのような一般的なミスを捕捉する開発用ミドルウェアを適用したapplyMiddlewareの呼び出し -
Redux DevTools Extension をセットアップするための
composeWithDevToolsの呼び出し
以下は、Redux Toolkit を使用したホットリローディングの例です:
import { configureStore } from '@reduxjs/toolkit'
import monitorReducersEnhancer from './enhancers/monitorReducers'
import loggerMiddleware from './middleware/logger'
import rootReducer from './reducers'
export default function configureAppStore(preloadedState) {
const store = configureStore({
reducer: rootReducer,
middleware: getDefaultMiddleware =>
getDefaultMiddleware().prepend(loggerMiddleware),
preloadedState,
enhancers: [monitorReducersEnhancer]
})
if (process.env.NODE_ENV !== 'production' && module.hot) {
module.hot.accept('./reducers', () => store.replaceReducer(rootReducer))
}
return store
}
これは確かにセットアッププロセスを大幅に簡素化します。
次のステップ
ストア設定をカプセル化してメンテナンスを容易にする方法がわかったので、Redux Toolkit の configureStore API を確認するか、Redux エコシステムで利用可能な拡張機能について詳しく調べてみてください。