banner
他山之石

他山之石

手を取り合ってシンプルなReduxを作ろう(四)

Redux の原理を理解することは、より良くそれを使用するのに役立ちます。本記事では、redux の複数のミドルウェアを統合する機能を実装します。

image

前回の記事では、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 でプロジェクトを起動し、ブラウザで操作すると、結果は以下の通りで、期待通りの効果が得られます。

image

読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。