アクションをディスパッチすることと、それがリデューサーに到達する瞬間の間に、サードパーティの拡張ポイントを提供します。 --- ミドルウェア
本文の完全なコードは Github でご覧ください:https://github.com/YanYuanFE/redux-app
// リポジトリをクローン
git clone https://github.com/YanYuanFE/redux-app.git
cd redux-app
// ブランチをチェックアウト
git checkout part-5
// インストール
npm install
// スタート
npm start
ビジネスニーズがより複雑になると、ディスパッチとリデューサーの中でビジネスロジックを単純に処理することは一般的ではなくなります。私たちが必要とするのは、組み合わせ可能で自由にプラグインできるプラグインメカニズムです。Redux は Koa のミドルウェアの考え方を参考にして、Redux のミドルウェアを実現しました。アクションを発行し、リデューサーを実行する間に、ミドルウェア関数を使用して store.dispatch を改造します。したがって、Redux のミドルウェアはディスパッチを強化するために存在します。
ミドルウェアの使用方法を振り返ると、ここでは redux-thunk ミドルウェアを例に、以前のカウンターに遅延カウント機能を追加します。つまり、ボタンをクリックしてから 2 秒後に + 1 操作を行います。Redux と react-redux は公式実装を使用しており、以下は改造されたコードです:
まずはカウンターコンポーネントで、src の components ディレクトリ内の Counter.js に以下のコードがあります:
import React, { Component } from 'react';
import PropTypes from 'prop-types';
class Counter extends Component {
constructor(props) {
super(props);
}
render() {
const { value, onIncrement, onDecrement, incrementAsync } = this.props;
console.log(this.props);
return (
<p>
クリック数: {value} 回
{' '}
<button onClick={onIncrement}>
+
</button>
{' '}
<button onClick={onDecrement}>
-
</button>
{' '}
<button onClick={incrementAsync}>
非同期でインクリメント
</button>
</p>
)
}
}
Counter.propTypes = {
value: PropTypes.number.isRequired,
onIncrement: PropTypes.func.isRequired,
onDecrement: PropTypes.func.isRequired,
incrementAsync: PropTypes.func.isRequired
};
export default Counter;
上記のコードでは、非同期で + 1 するためのボタンが追加され、クリックすると props 内の incrementAsync メソッドがトリガーされます。
次は 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 } = 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}
/>
</div>
);
}
}
const mapStateToProps = (state) => ({
counter: state
});
function onIncrement() {
return { type: 'INCREMENT' }
}
function onDecrement() {
return { type: 'DECREMENT' }
}
function incrementAsync() {
return dispatch => {
setTimeout(() => {
dispatch(onIncrement());
}, 2000)
}
}
const mapDispatchToProps = {
onIncrement,
onDecrement,
incrementAsync
};
export default connect(mapStateToProps, mapDispatchToProps)(App);
前回の記事と比較して、主に incrementAsync メソッドを定義し、それを mapDispatch に追加しました。incrementSync 内では、関数を返し、その内部で非同期操作を実行してアクションを発行します。通常のアクションはオブジェクト形式ですが、非同期アクションは関数を返します。このような状況を処理するためには、Redux のミドルウェアである redux-thunk を使用する必要があります。
次は src の index.js のコードです:
import React from 'react';
import ReactDOM from 'react-dom';
import thunk from 'redux-thunk';
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));
ReactDOM.render(
<Provider store={store}>
<App/>
</Provider>,
document.getElementById('root')
);
index.js では、react-thunk モジュールをインポートし、createStore 内で第二引数として渡し、Redux の API applymiddleware を使用して chunk ミドルウェアをラップします。
これが改良されたカウンターであり、プロジェクトを実行してブラウザでプレビューし、incrementAsync ボタンをクリックすると、期待通りの効果が得られます。以下の図のように:
applyMiddleware の実装#
applyMiddleware は Redux が提供するミドルウェアを使用するための API です。applyMiddleware の使用を振り返ります:
const store = createStore(counter, applyMiddleware(logger, thunk));
applyMiddleware は複数のミドルウェア引数を受け取り、返り値を createStore に渡す第二引数として使用します。
src の redux.js ファイル内では、まず元の createStore が第二引数を受け取るようにします。コードは以下の通りです:
export function createStore(reducer, enhancer) {
if (enhancer) {
return enhancer(createStore)(reducer);
}
}
createStore に第二引数 enhancer を渡し、enhancer は applyMiddleware でミドルウェアをラップする関数を呼び出します。次に、enhancer が存在するかどうかを確認し、存在する場合は enhancer を呼び出し、createStore と reducer の 2 つの引数を渡します。これにより、applyMiddleware は高階関数であり、新しい関数を返す必要があります。
次に applyMiddleware メソッドを公開します。Redux のミドルウェアは複数のミドルウェアをサポートしており、ここではまず 1 つのミドルウェアの使用をサポートします。
export function applyMiddleware(middleware) {
return createStore => (...args) => {
const store = createStore(...args);
let dispatch = store.dispatch;
const midApi = {
getState: store.getState,
dispatch: (...args) => dispatch(...args)
}
dispatch = middleware(midApi)(store.dispatch)
return {
...store,
dispatch
}
}
}
applyMiddleware の構造は多層のカリー化された関数であり、第一層の関数が実行されると関数が返され、この関数が createStore 関数の引数 enhancer になります。次にこの関数が createStore の引数として渡され、さらに新しい関数が返され、その関数が reducer 引数を受け取ります。内部で、まず createStore を呼び出して元の store と dispatch を取得し、midApi というオブジェクトをミドルウェア内部に渡します。midApi には getState と dispatch の 2 つのメソッドが含まれ、getState は store.getState メソッドに対応し、dispatch は store.dispatch メソッドに対応し、引数を透過的に渡します。以下はログミドルウェアのコードです:
const logger = store => next => action => {
console.log('dispatching', action);
let result = next(action);
console.log('next state', store.getState());
return result;
};
このように、ミドルウェア関数は層層包まれた匿名関数であり、第一層には store が渡され、第二層には次のミドルウェア、ここでは store.dispatch が渡され、第三層にはコンポーネント内で呼び出すときに action が渡されます。logger ミドルウェアは applyMiddleware 内で層層呼び出され、動的に store と next パラメータに値が割り当てられます。
次に applyMiddleware のコードを見て、getState と dispatch で構成されたクロージャ midApi を定義します。ミドルウェア関数 middleware は最初に midApi を渡して呼び出され、匿名関数を返します。以下のようになります:
next => action => {
console.log('dispatching', action);
let result = next(action);
console.log('next state', store.getState());
return result;
};
次に、この匿名関数を再度呼び出し、store.dispatch を next パラメータとして渡し、再び匿名関数を返します。以下のようになります:
action => {
console.log('dispatching', action);
let result = next(action);
console.log('next state', store.getState());
return result;
};
middleware の層層呼び出しによって新しい dispatch メソッドが生成され、新しい dispatch は store の元の dispatch メソッドを強化します。最後に、オブジェクトを返し、分割代入を使用して強化された dispatch が元の store.dispatch を上書きし、新しい store になります。最終的に、コンポーネント内で dispatch を発行するときに使用されるのは新しい dispatch メソッドです。
ここまでで、Redux がミドルウェアの使用をサポートするようになりました。次に、上記のプロジェクト内のカウンターの例を使用して、Redux と react-redux を自分で作成したファイルに置き換え、redux-thunk はそのままにしてプロジェクトを実行します。3 つのボタンを順にクリックすると、期待通りの効果が得られます。
redux-thunk ミドルウェアの作成#
上記で Redux がミドルウェアを使用できるようにしたので、次に自分で thunk ミドルウェアを作成してみます。src 内に thunk.js を新規作成します:
const thunk = ({dispatch, getState}) => next => action => {
return next(action)
}
export default thunk;
ミドルウェアの構造は上記の通りで、3 層のアロー関数です。第一引数に midApi を渡し、ここで dispatch と getState メソッドをデストラクチャリングして便利に使用します。第二引数には次のミドルウェア、つまり store.dispatch が渡され、第三引数には送信する必要がある action が渡されます。ミドルウェア関数内で、何もしない場合は next(action)を直接返します。つまり、action をそのまま dispatch します。もちろん、これでは何の意味もありません。以下に thunk のコードを追加します:
const thunk = ({dispatch, getState}) => next => action => {
// もし関数なら、実行する
if (typeof action === 'function') {
return action(dispatch, getState);
}
return next(action)
}
export default thunk;
これが redux-thunk のコードです。とてもシンプルです。まず、渡された action が function かどうかを判断し、function であればその action を実行し、dispatch と getState を渡します。以下のように、incrementAsync の返り値の関数の引数で dispatch と getState の 2 つの引数を受け取ることができます:
function incrementAsync() {
return (dispatch, getState) => {
setTimeout(() => {
dispatch(onIncrement());
}, 2000)
}
}
次に、実装した thunk ミドルウェアを検証します。src 内で redux-thunk を./thunk に置き換えます。ブラウザで 3 つのボタンを順にクリックすると、結果は以下のようになり、期待通りの効果が得られます。