Redux の原理を理解することは、より良くそれを使用するのに役立ちます。本記事では、redux の複数のミドルウェアを統合する機能を実装します。
前回の記事では、redux のミドルウェア機構を実装し、1 つのミドルウェアを渡す使い方をサポートしました。実際の redux では、applyMiddleware は複数のミドルウェアを渡すことをサポートしています。本記事では、redux を使用して複数のミドルウェアを統合します。
本記事の完全なコードは Github でご覧ください:https://github.com/YanYuanFE/redux-app
// リポジトリをクローン
git clone https://github.com/YanYuanFE/redux-app.git
cd redux-app
// ブランチをチェックアウト
git checkout part-6
// インストール
npm install
// スタート
npm start
ミドルウェアの統合#
複数のミドルウェアを使用するサンプルコードは以下の通りです:
const store = createStore(
reducer,
applyMiddleware(thunk, promise, logger)
);
元のプロジェクトで開発を続け、src の redux.js で applyMiddleware 関数を修正します。
export function applyMiddleware(...middlewares) {
}
単一のミドルウェア middleware の構造は以下の通りです:
store => next => action => {
let result = next(action);
return result;
};
複数の middlewares パラメータが渡された場合、パラメータを展開し、middlewares は配列となり、後の操作が容易になります。
export function applyMiddleware(...middlewares) {
return createStore => (...args) => {
const store = createStore(...args);
let dispatch = store.dispatch;
const midApi = {
getState: store.getState(),
dispatch: (...args) => dispatch(...args)
}
const middlewareChain = middlewares.map(middleware => middleware(midApi));
dispatch = compose(...middlewareChain)(store.dispatch);
return {
...store,
dispatch
}
}
}
複数のミドルウェアがある場合、ミドルウェア配列 middlewares に対して map メソッドを実行し、各ミドルウェアを 1 回実行して midApi を渡し、新しい配列 middlewareChain を返します。
middlewareChain には、ミドルウェアが 1 回実行された後に返される関数 [mid1, mid2, mid3] が保存されます。それぞれの mid の構造は以下の通りです:
next => action => {
let result = next(action);
return result;
};
次に、各 mid メソッドを順次実行し、関数を返すために compose メソッドが必要です。最後に store.dispatch パラメータを渡します。
compose メソッドの役割は以下の通りです:
compose(fn1, fn2, fn3)
fn1(fn2(fn3)))
compose は一連の関数をパラメータとして受け取り、一連の関数パラメータをネストして順次呼び出します。
以下は compose の実装です:
function compose(...funcs) {
if (funcs.length === 0) {
return arg => arg;
}
if (funcs.length === 1) {
return funcs[0]
}
return funcs.reduce((ret, item) => (...args) => ret(item(...args)))
}
compose メソッドでは、一連の関数パラメータを受け取り、展開し、funcs は配列となります。
funcs.length が 0 の場合、つまりパラメータが 1 つもない場合、デフォルト関数を返します;1 つのパラメータが渡された場合は、最初のパラメータを直接返します;複数の関数パラメータが渡された場合、配列の reduce メソッドを使用して funcs 配列を左から右に順次実行します。この関数内で、ret は前回実行した関数の返り値であり、初期値が指定されていない場合、最初の実行時は配列の最初のパラメータとなります。item は現在処理中の配列要素です。
compose (fn1, fn2, fn3) を実行すると、reduce メソッド内の ret と item の各実行結果は以下の通りです:
最初の実行では、ret は fn1、item は fn2 で、fn1(fn2())を返します;
2 回目の実行では、ret は fn1(fn2())、item は fn3 で、結果は fn1 (fn2 (fn3()))) となります。
これで、redux は複数のミドルウェアの使用をサポートするようになりました。
ミドルウェアを作成してテスト#
複数のミドルウェアをテストするために、ここで配列アクションをサポートするシンプルなミドルウェアを作成します。src ディレクトリに redux.array.js を新規作成します。
コードは以下の通りです:
const arrayThunk = ({dispatch, getState}) => next => action => {
if (Array.isArray(action)) {
action.forEach(v => dispatch(v))
}
return next(action)
}
export default arrayThunk;
上記のコードでは、arrayThunk ミドルウェアを定義し、Array.isArray メソッドを使用して action が配列かどうかを判断します。もし配列であれば、action を遍歴して dispatch します。
次に、元のカウンターアプリケーションにボタンを追加し、arrayThunk ミドルウェアを使用してクリックするたびに 2 を加算します。
components/Counter.js を以下のように修正します:
import React, { Component } from 'react';
import PropTypes from 'prop-types';
class Counter extends Component {
constructor(props) {
super(props);
this.incrementAsync = this.incrementAsync.bind(this);
this.incrementIfOdd = this.incrementIfOdd.bind(this);
}
incrementIfOdd() {
if (this.props.value % 2 !== 0) {
this.props.onIncrement();
}
}
incrementAsync() {
setTimeout(this.props.onIncrement, 1000);
}
render() {
const { value, onIncrement, onDecrement, incrementAsync, addTwice } = this.props;
console.log(this.props);
return (
<p>
クリックされた回数: {value} 回
{' '}
<button onClick={onIncrement}>
+
</button>
{' '}
<button onClick={onDecrement}>
-
</button>
{' '}
<button onClick={incrementAsync}>
非同期で加算
</button>
{' '}
<button onClick={addTwice}>
+2
</button>
</p>
)
}
}
Counter.propTypes = {
value: PropTypes.number.isRequired,
onIncrement: PropTypes.func.isRequired,
onDecrement: PropTypes.func.isRequired,
incrementAsync: PropTypes.func.isRequired,
addTwice: PropTypes.func.isRequired,
};
export default Counter;
Counter.js にボタンを追加し、クリックで props 内の addTwice メソッドをトリガーします。
src の App.js で、コードを以下のように修正します:
import React, { Component } from 'react';
import { connect } from './react-redux';
import Counter from './components/Counter';
import logo from './logo.svg';
import './App.css';
class App extends Component {
render() {
const { onIncrement, onDecrement, counter, incrementAsync, addTwice } = this.props;
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<h1 className="App-title">Reactへようこそ</h1>
</header>
<p className="App-intro">
始めるには、<code>src/App.js</code>を編集して保存してください。
</p>
<Counter
value={counter}
onIncrement={onIncrement}
onDecrement={onDecrement}
incrementAsync={incrementAsync}
addTwice={addTwice}
/>
</div>
);
}
}
const mapStateToProps = (state) => ({
counter: state
});
function onIncrement() {
return { type: 'INCREMENT' }
}
function addTwice() {
return [{ type: 'INCREMENT' }, { type: 'INCREMENT' }]
}
function onDecrement() {
return { type: 'DECREMENT' }
}
function incrementAsync() {
return (dispatch, getState) => {
setTimeout(() => {
dispatch(onIncrement());
}, 2000)
}
}
const mapDispatchToProps = {
onIncrement,
onDecrement,
incrementAsync,
addTwice,
};
export default connect(mapStateToProps, mapDispatchToProps)(App);
addTwice メソッドを追加し、配列アクションを発行します。配列内に 2 つのカウントを増やすアクションを定義し、mapDispatch に追加します。App コンポーネント内で props から addTwice メソッドを取得し、Counter コンポーネントに渡します。
index.js で、arrayThunk ミドルウェアをインポートする必要があります。
import React from 'react';
import ReactDOM from 'react-dom';
import thunk from './thunk';
import arrThunk from './redux-array';
import { Provider } from './react-redux';
import './index.css';
import App from './App';
import {applyMiddleware, createStore} from './redux';
import counter from './reducers';
const store = createStore(counter, applyMiddleware(thunk, arrThunk));
ReactDOM.render(
<Provider store={store}>
<App/>
</Provider>,
document.getElementById('root')
);
index.js で arrayThunk をインポートし、applyMiddleware に渡します。
npm start でプロジェクトを起動し、ブラウザで操作すると、結果は以下の通りで、期待通りの効果が得られます。