React + Flux入門


この記事ではReactとFluxのアーキテクチャの概要と、実際にReactとFluxを使ってアプリを開発する方法を簡単に説明します。特にFluxにフォーカスしています。

併せて、ReactとFluxを使ったアプリを簡単に作れるようにするstarter-react-fluxというツールもnpmに公開しました。使い方はこちらです。これを使えば、React + Flux + Babel + Webpack + ESlint + Prettier + Jest他ポピュラーなライブラリで構成されるPWAをすぐに生成できます。また、JavaScriptだけでなくTypeScriptにも対応しています。この記事は入門編なのでJavaScriptで説明しています。

Fluxとは

Facebookが提唱したUIを持ったアプリを作るためのアーキテクチャです。アーキテクチャを実現するための同名のライブラリもあります。

アーキテクチャとしてのFlux

Facebookは次のような図でFluxを説明しています。

In-Depth Overview

ざっくり言うと、View等でイベントが発生した結果Actionが生成されて、Dispatcher経由でStoreに渡り、Storeの内容が再度Viewに反映されるという図です。データの流れが一方通行(uni-dierctional)であることがFluxの重要な点です。

ただし、この説明だけだと、Actionは誰がどう作るのか、Storeが何なのかが明確ではありません。もう少しだけ詳しく説明すると次のようなものです。

  1. Viewが、Storeに格納された状態(state)を元に表示されます。
  2. Viewでイベント(onClickなど)が発生すると、対応するイベントハンドラ(例. handleClick())が呼ばれます。
    • Viewの例: React component
  3. イベントハンドラがAction Creatorという関数を呼び出します。
    • Action Creatorが行う処理の例: APIコール、DB操作、計算など
  4. Action Creatorは、Actionとよばれるデータ生成して、ActionDispatcherdisptach()メソッドに渡します。
    • Actionの例: REST APIのレスポンス、DBから取得したデータ
  5. StoreActionを受け取って、StoreのstateとActionの内容を元に、stateを更新します。
  6. Storeの状態(state)が、再びViewに反映されます。

ライブラリとしてのFlux

FacebookはFluxアーキテクチャを実現するためのライブラリをアーキテクチャと同じFluxという名前で提供しています。FacebookのFluxライブラリは、Fluxアーキテクチャに登場するDispatcher, Store, Viewに対応するコンポーネントを提供しています。

Fluxの要素名 対応ライライブラリ名
Dispatcher Dispatcher
Store ReduceStore
View Container, React Component

注: Fluxはv2.1.0からReduceStoreというStoreのコンポーネントとContainerというViewのコンポーネントが追加されました。これらは大変強力なコンポーネントで、ReduceStoreの状態をContainerに自動で反映してくれます。ReduceStore登場以前は、Storeの変更をViewに反映させるためにEvent Emitterが必要で記述が多く面倒でしたが、現在ではそのような処理は不要です。

Fluxの実装方法

基本的に以下のファイルを実装していきます。

  • ActionCreators: 何らかの処理を行い、結果からActionというデータを作り出して、Dispatcherに渡します。
  • Dispatcher: 渡されたActionをStoreに配信するHub的な役割です。Actionに関する統一的な処理を行う際などにコードを追加する事はありますが、普段はあまり気にすることはないかもしれません。
  • ReduceStore: アプリの状態を管理する非永続的なデータストアです。現在のストアの状態と新たなActionを元に新たな状態を作ります。
  • Component: 単なるReactコンポーネントです。
  • Container: ReduceStoreのデータを自動で受け取るだけの形式的なReact Componentです。アプリのロジックを書くことは推奨されていません。

Fluxの単方向データフローのデータ観点でこれを整理すると以下のような流れです。

React Component 
---[ event  ]---> ActionCreator
---[ action ]---> Dispatcher 
---[ action ]---> ReduceStore 
---[ state  ]---> Container 
---[ props  ]---> React Component 

さて、次はFluxの具体的な実装方法を要素別に見ていきます。

Action

  1. ActionCreatorsのファイルを作成します。
  2. ユーザイベントに対応するロジックやビジネスロジックを記述します。
  3. ロジックの処理結果を元にActionを作成してDispatcherに渡します。

用語の整理

Fluxを説明するドキュメントには、Action, Action Creator, Action Creator*s*とAction関連で似た言葉が出てきてわかり辛いので解説します。

Action

  • ユニークな識別子とデータから構成されるオブジェクトです。

    例:

    {
       type: "ユニークな識別子",
       payload: データ(APIのレスポンス等)
    }
    

    Actionのスキーマは規定されていませんが、自由だと不便なのでFlux Standard ActionというActionのスキーマ仕様が提案されています。starter-react-fluxでもFlux Standard Actionを採用しました。

  • ActionはDispatcher経由でStoreに渡されます。

  • ActionCreator: Actionを生成して、ActionをDispatcherに渡す関数です。ViewのonClick等から呼び出されるケースが多いです。

  • ActionCreators: 複数のActionCreatorが定義されたファイルです。

実例

import AppDispatcher from '../AppDispatcher';

const NewsActionCreators = {   //ActionCreators
  fetchNews(arg1, arg2) {      //ActionCreator

    //ユーザイベント等に対応するロジックを記述

    AppDispatcher.dispatch({   //Actionオブジェクト
      type: 'ACTION_TYPE_001',  //Actionを識別する一意の文字列, 
      payload: response,          //ロジックの結果をいれる
    });
  },

};

export default NewsActionCreators;

GitHub上の実装例

Store: ReduceStore

アプリケーションで使う状態(state)を管理します。

  1. ReduceStoreを拡張したクラスを作成します。
  2. stateの初期値を、getInitialState()関数の戻り値として定義します。
    • 例: Map, Array, String, Number, Object,..
    • 例: blogの記事一覧を格納するReduceStore -> blog記事を古い順に格納したArray
  3. 「現在のstate」と「dispatcher経由で受け取ったaction」から「新たなstate」を作成するreduce(state, action)を実装します。
    • 例: blogのコメント一覧の場合、「現在のコメント一覧(Array)」とActionオブジェクト内の「新たなコメント一覧(Array)」を結合したArrayをreduceでreturn。

実例

import { ReduceStore } from 'flux/utils';
import AppDispatcher from '../dispatchers/AppDispatcher';

class NewsStore extends ReduceStore {
  getInitialState() {
    return []; //stateの初期値を定義
  }

  reduce(state, action) {
    switch (action.type) {
      case 'ACTION_TYPE_001':
        //actionの内容を新たなstateとする (action.dataはarrayと仮定)
        return action.payload; 
      case 'ACTION_TYPE_002':
        //現在のstateとactionの内容から新たなstateを作成
        return state.concat(action.data);
      default:
        //現在のstateをそのまま返す
        return state;
    }
  }
}

export default new NewsStore(AppDispatcher);

GitHub上の実装例

※ FacebookのFlux(flux/utils)では、Storeを実現するクラスとしてStore, ReduceStore, MapStoreの3種類を用意していますが、Storeは煩雑でMapStoreはv3.0.0で削除されたのでReduceStoreだけを考えれば良いです。

View: Container

ReactComponentの一種です。ただし、ReduceStoreのstateの内容を取得して配下のReactコンポーネントに渡すだけの特殊なReact Componentです。UIやロジックをContainerの中で実装するのは非推奨で、UIやロジックは子供のReactComponentで実装します。

  1. ReactComponentを作成して、Container.create()でContainer化します。
  2. getStores()で、利用したいReduceStoreを指定します。
  3. calculateState()で、ReduceStoreからstateを取得して、Containerで使う際のスキーマを定義します。

実例

import React, { Component } from 'react';
import { render } from 'react-dom';
import { Container } from 'flux/utils';
import NewsStore from './stores/NewsStore';
import FavStore from './stores/FavStore';

class _App extends Component {
  static getStores() {
    return [NewsStore, FavStore]; //利用したいReduceStore
  }

  static calculateState() {
    return { //container内で`this.state.KEY_NAME`でアクセス可能
      news: NewsStore.getState(),
      favs: FavStore.getState(),
    };
  }

  render() {
    //通常のReactComponentのように書く
  }
}

const AppContainer = Container.create(_App);
render(<AppContainer />, document.getElementById('root'));

GitHub上の実装例

View: React Component

普通のReact componentです。Containerのstateをpropsとして受け取ります。

付録: starter-react-fluxについて

更新履歴

  • v4: 2019/3

    • Progressive Web Application (PWA)に対応
    • UIアップデート
  • v5: 2019/8

    • JavaScriptとTypeScriptの両方に対応
      • --tsオプションをつけるとTypeScriptのプロジェクトを生成。(標準はJavaScript)
    • npmとyarnの両方に対応
      • --yarnオプションをつけるとyarnでライブラリをインストール。(標準はnpm)

使い方

  1. React/Fluxアプリを作ります。

    JavaScriptの場合

    npx starter-react-flux init
    

    TypeScriptの場合

    npx starter-react-flux init --ts
    
  2. 作成したReact+Fluxアプリを起動します。

    npm start
    
- アプリが起動します。
- ファイルを修正すると自動でリロードされます。

![Screen Shot](https://raw.githubusercontent.com/SokichiFujita/starter-react-flux/master/images/app1.png)
  1. ESlintで静的解析を行います。

    npm run lint
    
  2. PrettierでESlintのルールを満たすようコードをある程度自動修正します。

    npm run lint
    
  3. Jestでテストを実行します。

    npm test
    
  4. アプリをビルドします。ビルドしたアプリはpublic配下に格納されます。

    npm run build