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

ストアの設定

「Redux の基本」チュートリアルでは、Todo リストアプリの例を作成することで、Redux の基本的な概念を紹介しました。その中で、Redux ストアの作成と設定方法について説明しました。

ここでは、追加の機能を追加するためにストアをカスタマイズする方法について説明します。まず、「Redux の基本」パート 5:UI と 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 と、loggerMiddleware および monitorReducerEnhancer、および Redux が提供する 2 つの追加関数 (applyMiddlewarecompose) をインポートする必要があります。

  • 次に、applyMiddleware を使用して、loggerMiddlewarethunkMiddleware をストアのディスパッチ関数に適用するストアエンハンサーを作成します。

  • 次に、compose を使用して、新しい middlewareEnhancermonitorReducerEnhancer を 1 つの関数に合成します。

    これは、createStore に渡すことができるエンハンサーが 1 つだけであるため必要です。複数のエンハンサーを使用するには、まずこの例に示すように、エンハンサーを 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 thunkMiddleware 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, thunkMiddleware)
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 thunkMiddleware 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, thunkMiddleware]
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 thunkMiddleware 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, thunkMiddleware]
const middlewareEnhancer = applyMiddleware(...middlewares)

const enhancers = [middlewareEnhancer, monitorReducersEnhancer]
const composedEnhancers = composeWithDevTools(...enhancers)

const store = createStore(rootReducer, preloadedState, composedEnhancers)

return store
}

そして、それだけです。

devtools 拡張機能がインストールされたブラウザ経由でアプリにアクセスすると、強力な新しいツールを使用して探索とデバッグを行うことができます。

ホットリローディング

開発プロセスをより直感的にすることができるもう 1 つの強力なツールはホットリローディングです。これは、アプリ全体を再起動せずにコードの一部を置き換えることを意味します。

たとえば、アプリを実行し、しばらく操作した後、リデューサーの 1 つに変更を加えることにした場合に何が起こるかを考えてみましょう。通常、これらの変更を加えると、アプリが再起動し、Redux の状態が初期値に戻ります。

ホットモジュールリローディングを有効にすると、変更したリデューサーのみがリロードされるため、毎回状態をリセットせずにコードを変更できます。これにより、開発プロセスが大幅に高速化されます。

Redux リデューサーと React コンポーネントの両方にホットリローディングを追加します。

まず、configureStore 関数に追加しましょう。

import { applyMiddleware, compose, createStore } from 'redux'
import thunkMiddleware 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, thunkMiddleware]
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 エコシステムで利用可能な拡張機能の一部を詳しく見てください。