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

このページは 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-reduxProviderコンポーネントに渡しています。このコンポーネントはコンポーネントツリーの最上位にレンダリングされます。

これにより、アプリ内でreact-reduxconnectを通じて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、自前のloggerMiddlewaremonitorReducerEnhancer、さらにReduxが提供するapplyMiddlewarecompose関数をインポートします

  • 次にapplyMiddlewareを使用し、loggerMiddlewarethunkミドルウェアをストアのdispatch関数に適用するストアエンハンサーを作成します

  • 続いてcomposeを使用し、新規作成したmiddlewareEnhancermonitorReducerEnhancerを単一の関数に合成します

    これは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
}

この関数は前述の手順に沿っていますが、将来の拡張を見越してロジックを分割しています:

  • middlewaresenhancers は両方とも配列として定義され、それらを処理する関数から分離されています。

    これにより、異なる条件に基づいて簡単にミドルウェアやエンハンサーを追加できます。

    例えば、開発モード時のみ特定のミドルウェアを追加する一般的なパターンは、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 モジュールを監視し、変更時に更新された rootReducerstore.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 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 エコシステムで利用可能な拡張機能について詳しく調べてみてください。