maesblog

Reduxの実装とReactとの連携を超シンプルなサンプルを使って解説

玉石混合状態にあったFluxのフレームワークも、ここ最近ではReduxが首一つ抜け出したような感じとなっています。自分はFacebook/Flux派ではありましたが、先月発売された『WEB+DB PRESS vol.92』に掲載されていた伊藤直也さんのReduxの記事を読んで、Reduxを覚えてみようという気になりました。Redux自体はとてもシンプルで、とっつきやすいと思いました。ただReactとの連携はFacebook/Fluxと比べるとややこしい部分が多いかなといった印象です。ちょっとしたサンプルを作ってみたので、Reduxの実装方法とReactとの連携について紹介したいと思います。

redux

Reduxとは

Reduxは、Facebookの提唱するFluxアーキテクチャに基づいて(影響を受けて)設計されたJavaScriptフレームワークです。作者は@dan_abramov氏。

Fluxとは

Fluxというと、お馴染みとなっている以下の図の通り、アプリケーションをAction -> Dispatcher -> Store -> View (React)という4つの役割を持った部品で構築し、データは必ず一方向にしか流れないように設計するといったアプリケーションを構築するためのアーキテクチャです。

fluxアーキテクチャ

Reduxの特徴

Reduxはとてもシンプルです。大きくActions -> Reducers -> Storeの3つの部分から構成されます。上記のFluxの影響を受けていることから、データの流れは一方向になるようになっています。Storeが状態(state)を持ち、Actionが発生した際に、Reducerを使ってStoreの状態(State)を更新するといった仕組みとなります。

それから大事なことですが、状態(state)を格納するStoreはアプリケーションに必ず1つのみしか設けられません。1つのStoreですべての状態(state)を管理することになります。

Viewに関しては、Storeの用意するAPIを使うことで、stateの更新の購読と、stateの取得を行うことができるので、好きなようにしてくださいというスタンスです。React-Reduxというモジュールを使うと、Viewの実装にReactを使うことができるようになります。今回の記事では、このReact-Reduxを使ったReactとReduxの連携についても書いていく予定です。

Reduxの基本3原則

Reduxには以下のような基本3原則があります。これも一応頭の片隅に入れておくと良いでしょう。

  • Single source of truth

    アプリケーション全体のstateはひとつのstoreの中のobjectツリーに格納されます。

  • State is read-only

    action(何が起こるか記述されているオブジェクト)を発火することが、stateを更新する唯一の方法です。

  • Changes are made with pure functions

    stateがactionによってどのように変更されるか指定するために、純粋な(副作用のない)reducerを書きます。

Reduxの実装サンプル

サンプルはとても単純です。フォームに入力したテキストを表示させるだけのものとなっています。ReduxとReactを使って実装しています。

以下は、上記のサンプルを実装したサンプルコードです。簡単に説明するために、ファイルの分割などはせず、1つのファイルに全部書いています。参考にどうぞ。

Reduxの導入

早速Reduxのインストールから導入するまでを説明していきます。まず前提としてNode.js(npm)環境を用意しておく必要があります。説明は以下を参考にしてください。

それから、今回は説明を簡単にするために、ファイルの分割などはせず、1つのファイルにコードを全部書いていきます。

Reduxのドキュメントも合わせてご参照ください。

Reduxのインストール

npm経由でReduxをインストールします。まずプロジェクト用のディレクトリを作成し、そのディレクトリに移動してから、以下のコマンドを実行します。そうするとプロジェクトディレクトリ内にReduxがインストールされます。

$ mkdir reduxProject
$ cd reduxProject
$ npm init
$ npm install --save redux
CLI

Reduxのインポート

次に実際にコードを書いていくためのapp.jsファイルを作成し、そのファイル内にインストールしたReduxをインポートします。createStoreメソッドを読み込むようにします。

import { createStore } from 'redux';
app.js

ES6とJSXのコンパイル環境(Babel)の準備

今回のコードはES6(ES2015)構文で書いていきます。最終的にブラウザで読み込めるES5に変換する必要があります。それから、Viewで使用するReactではJSX構文を使用するため、こちらも通常のJavaScriptに変換する必要があります。従って、こうしたコンパイル環境の構築も必要です。詳細は当ブログの以下の記事を参考にしてください。

追記: 以下は当記事を公開後に書いた記事です。こちらも参考にしてください。


それでは、以下よりReduxの実装について説明していきます。

Storeの実装

「Store」は状態を保持する役割を持ち、ActionとReducerをまとめるオブジェクトです。以下のような特徴があります。

  • アプリケーションの状態(state)を保持します
  • getState()メソッドを通して状態(state)へのアクセスを許可します
  • dispatch(action)メソッドを通して状態(state)の更新を許可します
  • subscribe(listener)メソッドを通してリスナーを登録します
  • subscribe(listener)メソッドによって返された関数を通してリスナーの登録解除をハンドリングします

Storeの実装

Storeは、インポートしたReduxのcreateStore()メソッドを使って作成します。その際に、createStore()メソッドの引数として、「関数」として定義したReducer(後ほど説明します)と初期state(オブジェクト)を渡すようにします。今回はStoreに持たせるのは、シンプルにvalueという状態1つだけとします。

/* Storeの実装 */

// 初期state変数(initialState)の作成
const initialState = {
  value: null,
};
// createStore()メソッドを使ってStoreの作成
const store = createStore(formReducer, initialState);
app.js(Storeの実装)

Storeの使用方法

今回は、React-Reduxを使用して、Viewの実装をReactで行うようにしますが、Reactを使用しない場合は、上記で説明したStoreオブジェクトのメソッドを使って、Viewの実装を行うことになります。上記のようにStoreを作成したら、以下のような使い方をします。

// ActionをReducerに伝播
store.dispatch(actionCreators());


// stateの状態を購読。状態に変化があったらリスナーを実行
store.subscribe(() => {

  /* リスナーの処理を書く */

  // stateを取得
  store.getState();
});
app.js(Viewの実装)

Actions/Action Creatorsの実装

「Action」は、アプリケーションからStoreにデータを送る情報のペイロードです。Storeにおける唯一の情報源となります。Actionは、先ほど作成したstoreのdispatch()メソッドの引数に渡し、Storeに送られます。

Actionの実装

Actionは「アクション名」(Reducerで処理を判別するため)と「状態の値」を持った単なるオブジェクトです。作成したActionは、「Action Creator」と呼ばれる関数の戻り値にセットします。今回は、SENDという名前(type属性)と、Viewから受け取ったvalueという値を持ったActionを作り、さらにsend()という関数(Action Creator)を作って、その戻り値としてActionを返すようにします。

/* Actionの実装 */

// Action名の定義
const SEND = 'SEND';

// Action Creator
function send(value) {
  // Action
  return {
    type: SEND,
    value,
  };
}
app.js(Actionの実装)

Storeのdispatch()メソッドの引数にAction Creatorを渡すことで、ActionがReducerに送られます。

Reducersの実装

「Reducer」は、Actionに呼応してアプリケーションの状態(state)をどのように変化させるか指定する役割を持った関数です。受け取ったAcitonのタイプ属性を見て、対応するActionの値を用いて、Storeのsateを更新します。上記で説明した通り、Storeを作成する際に、ReducerをcreateStore()メソッドの第一引数として渡すことで、Store内でstateの更新が行えるようになっています。

Reducerの実装

Reducerは、「現在の状態(state)」と「受け取ったAction」を引数に取り、新しい状態を返す関数として実装します。switch文を使って、受け取ったActionの名前(type)を判別して処理を書くようにします。それから、Reduxの特徴的な部分となりますが、stateの更新を行う際にES2015のObject.assign()メソッドを使用しています。これは、stateそのものを変更させないようにするためです。Object.assign()メソッドを使用する際はpolyfillの使用が推奨されています。

今回の実装では、ActionのtypeがSENDの場合は、受け取ったActionのvalue値でStoreのstateを更新し、それ以外の場合は、元々のstateを返すようにしています。

// Reducer
function formReducer(state, action) {
  switch (action.type) {
    case SEND:
      return Object.assign({}, state, {
        value: action.value,
      });
    default:
      return state;
  }
}
app.js(Reducerの実装)

ちなみに、Reducerは複数作ることができます。ただし、Storeを作成する際は、一つのReducerをcreateStore()メソッドにセットするようになっています。そこで、Reducerを複数作った場合は、Reduxで用意されているcombineReducers(reducers)メソッドを使って一つにまとめるようにします。

import { combineReducers } from 'redux';

const reducer1 = (state, action) => {};
const reducer2 = (state, action) => {};
const reducer3 = (state, action) => {};

const reducer = combineReducers({
  reducer1,
  reducer2,
  reducer3,
});
Reducerの統合例

Reduxの実装の説明は以上となります。こうやってみるとReduxはかなりシンプルであることがわかると思います。Viewの実装も、Storeのところで説明したようにStoreで用意されているメソッドを使って行うことで実現可能です。他にもMiddlewareやStore enhancerなど覚える必要があるものもありますが、とりあえずこれだけ覚えておけばある程度のものは作れるようになるかと思います。

ただやはりReduxとReactは親和性が高く、Viewの実装はReactで行いたいところですね。ReactとReduxの連携について、以下より説明していきます。

Reactとの連携1: React-Reduxを導入する

ReduxとReactを連携させるには、React-Reduxというnpmのパッケージを使用します。

React-Reduxのインストール

React-ReduxはReduxには含まれていないので、新たにインストールする必要があります。以下のコマンドでワーキングディレクトリ内にReact-Reduxをインストールします。

$ npm install --save react-redux
CLI

React-Reduxのインポート

React-Reduxをインストールしたら、上のReduxの実装の説明で使用したapp.jsにインポートします。今回は、Providerクラスとconnectメソッドを読み込むようにします。これらの詳細は後ほど説明します。

import { createStore } from 'redux';
import { Provider, connect } from 'react-redux';
app.js(React-Reduxのインポート)

Reactとの連携2: Reactを導入する

次にViewを実装するためのReactを導入します。

Reactのインストール

以下のコマンドでワーキングディレクトリ内にReactとReact-domをインストールします。

$ npm install --save react react-dom
CLI

ReactとReact-domのインポート

React-Reduxと同様、上のReduxの実装の説明で使用したapp.jsにReactとReact-domをインポートします。

import React from 'react';
import ReactDOM from 'react-dom';
import { createStore } from 'redux';
import { Provider, connect } from 'react-redux';
app.js(Reactのインポート)

今回は、Reactに関する知識はある程度あるものとして話を進めていきます。当ブログではReactについての記事もいくつか書いているので、Reactについてはこちらをご参照ください。

Reactとの連携3: Viewを実装する

React-ReduxとReactを導入したら、早速連携したいところですが、まずはReactでViewを書いていきます。ここで大事なポイントがあります。ReduxのViewとしてReactを使う際は、Reactのコンポーネントを以下の2つのコンポーネントのどちらかとして実装するということです。

  • Container components: 機能に関するコンポーネント。いわゆるFluxでいうContainerです。主に直接Reduxと連携するコンポーネントで、ReduxのStoreの状態(state)を購読し、またReduxのActionをDispatchする役割を持ち、データを取得したり、stateの更新を行ったりします。主に親コンポーネントがこの役割を担います。
  • Presentational Components: 見た目に関するコンポーネント。Container componentsからpropsを通してデータを受け取り、Viewを構築します。また同様にpropsから受け取ったコールバックを実行します。

この考え方に関する詳細は、以下をご参照ください。結構大事な部分となります。

Container Componentsの実装

親コンポーネントをContainer Componentsとして実装します。子コンポーネントとして、FormInputコンポーネントとFormDisplayコンポーネントを紐付けるようにしています。それから、Propsを通して、ReduxのStoreと連携するようにしていますこれは後ほど説明しますが、React-Reduxのconnect()()メソッドを使って実現しています。

// View (Container Components)
class FormApp extends React.Component {
  render() {
    return (
      <div>
        <FormInput handleClick={this.props.onClick} />
        <FormDisplay data={this.props.value} />
      </div>
    );
  }
}
FormApp.propTypes = {
  onClick: React.PropTypes.func.isRequired,
  value: React.PropTypes.string,
};
app.js(Container Componentsの実装)

Presentational Componentsの実装

Presentational Componentsは、見た目を実装するコンポーネントです。Container Componentsの子コンポーネントとして実装します。データやコールバック関数などをすべてPropsを通して親であるContainer Componentsから受け取るようにします

こちらはテキストの入力フォームと送信ボタンを持ったコンポーネントになります。入力フォームはUncontrolled Componentsとなっています。ボタンを押した際に実行されるイベントハンドラ関数には、Container ComponentsからPropsを通して受け取ったコールバック関数がセットされていて、ReduxのActionがDispatchされるようになっています

// View (Presentational Components)
class FormInput extends React.Component {
  send(e) {
    e.preventDefault();
    this.props.handleClick(this.myInput.value.trim());
    this.myInput.value = '';
    return;
  }
  render() {
    return (
      <form>
        <input type="text" ref={(ref) => (this.myInput = ref)} defaultValue="" />
        <button onClick={(event) => this.send(event)}>Send</button>
      </form>
    );
  }
}
FormInput.propTypes = {
  handleClick: React.PropTypes.func.isRequired,
};
app.js(Presentational Componentsの実装)

こちらは入力フォームに入力されたテキストを表示させるコンポーネントになります。こちらもContainer ComponentsからPropsを通して、valueの値を受け取るようになっています。

// View (Presentational Components)
class FormDisplay extends React.Component {
  render() {
    return (
      <div>{this.props.data}</div>
    );
  }
}
FormDisplay.propTypes = {
  data: React.PropTypes.string,
};
app.js(Presentational Componentsの実装)

Reactとの連携4: ReactとReduxを連携させる

Viewを作成したら、最後にReact-Reduxを使ってReduxと連携させます。

Reactに連携させるReduxのStoreを渡す

ReactをReduxのViewとして機能させるには、まずどのReduxのStoreと連携させるかを決める必要があります。そこで、React-Reduxのインポートの際に読み込んだProviderクラスを使っていきます。

これはReactのコンポーネントとなっており、このProviderの子コンポーネントに、上で作成したReactのContainer Component(この後説明するReact-Reduxのconnect()()メソッドでContainer化しておきます)を紐付けます。その際にstore属性を通して、Container ComponentにReduxの対象となるStoreを渡します。こうすることで、ReactとReduxが連携されます。

ProviderコンポーネントとReactのContainer Componentの連携は、React-domのrenderメソッドの引数内で行います。

// Rendering
ReactDOM.render(
  <Provider store={store}>
    <AppContainer />
  </Provider>,
  document.querySelector('.content')
);
app.js(Providerを使ったレンダリング部分の実装)

連携したReduxのStoreをPropsを通してReactで使えるようにする

ReduxとReactを連携させたら、渡されたReduxのStoreをReact内で扱えるようにします。それにはReact-Reduxのインポートの際に読み込んだconnectメソッドを使用します。connectメソッドはカリー化されているので、見た目はちょっとややこしいですが、connectメソッドの引数にmapStateToProps関数とmapDispatchToProps関数を渡し、connectメソッドの戻り値にセットされている関数の引数にContainer Componentを渡すようにします。

mapStateToProps関数とmapDispatchToProps関数は、それぞれStoreのstatedispatchメソッドをpropsを通して、Container Componentで扱えるようにするものです。詳細は以下をご参照ください。

connect()()メソッドの一つ目の引数に、Storeのstate.value「value」として、dispatch(send())メソッドを「onClick()」として渡し、Container Component内でpropsを通して扱えるようにします。2つ目の引数にはラップする対象となるContainer ComponentのFormAppコンポーネントを渡して、AppContainerという変数に格納します。

// Connect to Redux
function mapStateToProps(state) {
  return {
    // propsを通して取得する際に使う名前: Storeのstateの値
    value: state.value,
  };
}
function mapDispatchToProps(dispatch) {
  return {
    // propsを通して取得する際に使う名前
    onClick(value) {
      // Storeのdispatchメソッド(引数はAction Creator)
      dispatch(send(value));
    },
  };
}
const AppContainer = connect(
  mapStateToProps,
  mapDispatchToProps
)(FormApp);
app.js(connectメソッドを使ったcontainerの実装)

AppContainerをレンダリングの際にReact-ReduxのProviderコンポーネントの子コンポーネントにセットすることで、ReactとReduxの連携は完了です。

ReduxとReactで実装したサンプルのソースコード

最後に、これまで説明してきたコードをひとつにまとめておきます。

import React from 'react';
import ReactDOM from 'react-dom';
import { createStore } from 'redux';
import { Provider, connect } from 'react-redux';

/* Actionsの実装 */

// Action名の定義
const SEND = 'SEND';

// Action Creators
function send(value) {
  // Action
  return {
    type: SEND,
    value,
  };
}

/* Reducersの実装 */

function formReducer(state, action) {
  switch (action.type) {
    case 'SEND':
      return Object.assign({}, state, {
        value: action.value,
      });
    default:
      return state;
  }
}

/* Storeの実装 */

const initialState = {
  value: null,
};
const store = createStore(formReducer, initialState);

/* Viewの実装 */

// View (Container Components)
class FormApp extends React.Component {
  render() {
    return (
      <div>
        <FormInput handleClick={this.props.onClick} />
        <FormDisplay data={this.props.value} />
      </div>
    );
  }
}
FormApp.propTypes = {
  onClick: React.PropTypes.func.isRequired,
  value: React.PropTypes.string,
};

// View (Presentational Components)
class FormInput extends React.Component {
  send(e) {
    e.preventDefault();
    this.props.handleClick(this.myInput.value.trim());
    this.myInput.value = '';
    return;
  }
  render() {
    return (
      <form>
        <input type="text" ref={(ref) => (this.myInput = ref)} defaultValue="" />
        <button onClick={(event) => this.send(event)}>Send</button>
      </form>
    );
  }
}
FormInput.propTypes = {
  handleClick: React.PropTypes.func.isRequired,
};

// View (Presentational Components)
class FormDisplay extends React.Component {
  render() {
    return (
      <div>{this.props.data}</div>
    );
  }
}
FormDisplay.propTypes = {
  data: React.PropTypes.string,
};

// Connect to Redux
function mapStateToProps(state) {
  return {
    value: state.value,
  };
}
function mapDispatchToProps(dispatch) {
  return {
    onClick(value) {
      dispatch(send(value));
    },
  };
}
const AppContainer = connect(
  mapStateToProps,
  mapDispatchToProps
)(FormApp);

// Rendering
ReactDOM.render(
  <Provider store={store}>
    <AppContainer />
  </Provider>,
  document.querySelector('.content')
);

今回のサンプルのソースコードはGitHubにもアップしていますので、git cloneでもしてお試ししていただければと思います。使用方法などはGitHubのREADMEに書いてあります。

まとめ

結構説明は長くなりましたが、Reduxのみであれば、Redux自体がかなりシンプルな作りとなっているので、理解もしやすいかと思います。むしろややこしいのがReact-Reduxを使ったReactとReduxの連携の方だと思います。特にconnectメソッドの部分で、自分も最初何のことやらと戸惑いました。実際に動きを確認しながら実装してみることで、それなりに理解できるようになるかと思います。

それから、やはりReduxはFacebookの提唱するFluxアーキテクチャを元に開発されているので、Flux自体もちゃんと理解しておくと、Reduxのそれぞれの機能やReactのContainer化の部分なども理解しやすくなると思います。当ブログではFluxについての記事もいくつか書いているので、Fluxについてはこちらをご参照ください。

いろいろ乱立していたFluxフレームワークもRedux一択といった流れになってきています。そろそろ本腰入れてReduxも覚えておいてもよい時期だと思います。

最後に伊藤直也さんによるReduxの記事が掲載されているWeb+DB PRESSを紹介しておきます。これはすごく貴重な情報源になるかと思います。

WEB+DB PRESS Vol.92
  • 『WEB+DB PRESS Vol.92』
  • 著者: 近藤 宇智朗 (著), 大和田 純 (著), 谷口 禎英 (著), 後藤 利博 (著), 黒瀧 悠太 (著), その他
  • 出版社: 技術評論社
  • ページ数: 168ページ
  • 発売日: 2016年4月23日
  • ISBN: 978-4774180540

関連記事

コメント

  1. ピンバック: React Redux Sample1 (Under constuction) | Professional Programmer

     

  2. ピンバック: React Redux Sample1 (Under construction) | Professional Programmer

     

      • 必須

      コメント