Help us understand the problem. What is going on with this article?

FluxによるReactアプリの状態管理 Flux・FluxUtils編

More than 1 year has passed since last update.

Flux及びFluxUtilsについて学習した内容をまとめたものです。
基本的には、本家サイトに記載されている内容を自分なりに日本語でまとめなおしたものです。
2019年5月時点の情報を基にしています。
Flux・FluxUtils

Reactの基本事項は既に習得されている方向けです。

Fluxとは

Reactによるアプリケーション構築を保管する為に考案された、設計パターンです。
Fluxによるアプリケーションは、下記のパートで構成されます。

  • Dispatcher: アプリケーションへの更新情報を集約・通知
  • Store: アプリケーションの状態を保持・管理
  • Action: アプリケーションの更新情報を得る為の内部API 
  • View: Reactコンポーネントによるインターフェース

Dispatcherは、アプリケーションに1つだけ存在します。
Storeは、アプリケーションの規模などに応じて1つ以上存在します。
Actionは、アプリケーションに発生しうる変更の数だけ存在します。
Vewは、Storeと接続したコンポーネントを頂点としてツリー構造を形成します。

Flux自体はあくまで設計思想なので、React以外の手法でViewを構築する際にも適用は可能です。

In Depth Overview | Flux
flux-concepts

FluxUtilsとは

Fluxを実装する為のフレームワークとして、公式より「Flux Utils」が提供されています。
必要最低限のシンプルな構成の為、必要に応じて他のフレームワークも利用してくれと、公式でも言っています。

Dispatcher | Flux
FluxUtilsのインストール | npm
導入サンプルのTodoアプリ | GitHub

しかし現在、FluxUtilsのドキュメントには下記文言が追記されています。

React 16.8 introduced Hooks. Much of the functionality of flux and Flux Utils can be reproduced using useContext & useReducer.
Using Flux with React Hooks

Reactは、Ver16.8で「Hooks」を導入しました。Flux及びFluxUtilsの機能の多くは、useContextとuseReducerを利用することで再現が可能です。

Fluxのリポジトリ内の多くのドキュメントやソースが、ここ1~2年は更新されていないようです。
これから導入する場合は、ReactHooksReduxなど、その他のフレームワークやライブラリーの方が良いのかもしれません。

Fluxのアプリの構成要素

Dispatcher

Dispatcherは、Fluxアプリケーションにおいて「ハブ」の役割を果たし、データの流れを管理します。
Actionを受け取り、予めstoreによって登録されたコールバック関数を用いて、各StoreにActionを配布するというシンプルな構造です。
特定のビジネスロジックは持ちません。actionの取捨選択はせず、全てStoreに配布されます。
このコールバック関数の実行順序を調整することで、各Store間の依存関係を管理します。
Storeは、コールバック関数の登録時に実行順序の指定ができます。
これにより特定の他のStoreの更新を待ってから、自身を更新することを可能にします。
この事は、アプリケーションの規模が大きくなるにつれて威力を発揮します。

A Single Dispatcher | Flux
Dispatcher | flux-concepts

Stores

Storeは、アプリケーションの状態の保持とビジネスロジックを担当しています。
アプリケーションの規模や構造などに応じて分割された、特定の領域を管理対象とします。
予めDispatcherにコールバック関数を登録しておき、変更の必要が発生した場合は、この関数の引数でactionを受け取ります。
コールバック関数内では、actionTypeを基にactionをSwitch構文を利用して識別し、必要に応じて対応する内部メソッドへ処理を振り分けるなどして、状態を更新します。
Storeでアプリケーションの状態を更新すると、それを通知するイベントを発行します。
通知を受け取ったViewは、新しい状態を基にコンポーネントを更新します。

TODOアプリのaction識別例
switch (actionType){
  case "ADD_TODO":
    return newState;
  case "EDIT_TODO":
    return this.getNewState(actionData);
  case "DELETE_TODO":
    // ・・・
  default:
    return currentState;
}

Stores | Flux
Store | flux-concepts

Views

Viewは、Reactによって提供される、アプリケーションのインターフェイス部分です。
描画されるアプリケーションの各コンポーネントは、ツリー構造を形成しています。
このツリーの最上位にあるコンポーネントは「controller-views」と呼ばれ、Storeが発行する通知を受け取ります。
そしてStoreから新しい状態を取得し、下位の描画用コンポーネントに情報を受け渡します。
また、ユーザから何らかの操作を受付け、そのイベントハンドラにてactionを発行し、Dispatcherに変更の必要性が発生したことを通知します。

※ 「Controller-Views」よりも、Reduxの「Container-Components」の方が名前として浸透しているかもしれません。
Fluxの導入サンプルとして公式が公開しているTODOアプリでも、フォルダー名・ファイル名には"container"を用いています。
この記事では、「Container-Components」の名称を採用します。

Views and Controller-Views | Flux
Views | flux-concepts

Actions

Actionは、Dispatcherを通じてStoreに渡す、アプリケーションへの変更内容を定義したデータです。
Storeで識別に利用するactionTypeと新しいデータで構成されます。
actionTypeだけの場合もあるかもしれません。

TODOアプリのaction例
// 新規追加
{
  type : "ADD_TODO",
  text : "牛乳2, 卵1, 納豆1"
}
// 削除
{
  type : "DELETE_TODO",
  id : "001"
}

このactionを発行してDispatcherに受け渡す為の、DispatcherのdispatchメソッドをラップしたヘルパーメソッドActionCreatorを予め用意しておきます。
このメソッドは、View内のイベントハンドラ内から呼び出されます。
その際にイベントに関する新しい情報を受け取り、actionTypeのラベルを付加してdispatchメソッドに渡します。
そしてDispatcherは、指定された順序で各Storeにactionを配布します。
View上でのユーザ操作以外にも、サーバから初期化情報や更新情報を受け取る際にも、ActionCreatorを通じてactionの発行が行われます。

Actions | Flux
Actions | flux-concepts

一方通行のデーターフロー

Fluxにおいて、データの流れは基本的に一方通行です。
一方通行に限定することで、コンポーネント間で相互に状態の変更を把握する必要が無くなり、管理が容易になります。
View上でのユーザ操作により変更の必要性が発生すると、ActionCreatorsを用いて変更内容を定義したactionを発行します。
actionは、その種類を識別するラベル「actionType」と変更に必要なデータを含んだオブジェクトです。
そのactionはDispatcherで集約され、各Storeに配布されます。
各Storeは配布された「action」を元に、必要に応じてアプリケーションの新しい状態を再定義し、Viewに通知します。
Viewは新しい定義を基に再描画します。
その他、WebAPIなどの外部からの変更も、actionの発行を通じてのみ状態の変更が行われます。

  • 更新の手続きをActionCreatorsに限定する
  • Dispatcher及びStoreで情報を一元管理する

これにより、アプリケーションの管理が煩雑化することを防ぎます。

各パートは独立しており、オブザーバーパターンの形式で連携しています。
DispatcherはStoreに対して変更の発生を一方的に通知し、Store側は通知を受け取り次第、必要に応じて更新処理を行います。
通知の内容によっては、何もしないかもしれません。
Storeは保持する状態に更新が発生すると、Viewに対してその事実を一方的に通知します。
Viewの各コンポーネントは必要に応じて更新を行うか、無視します。

Structure and Data Flow | Flux
Observer パターン | TECHSCORE

Todoアプリ

公式のFluxUtils導入サンプルはかなりリッチな造りで、FluxUtils以外のライブラリーも利用しています。
その為、ReduxのベーシックチュートリアルにあるTodoサンプルをベースにした、簡素なTodoサンプルを作成してみました。

この記事で作成するTodoアプリの完全なソースコードはこちらです。
todoapp-flux-utils
動作のサンプル

ReactのJSX記法を利用する為、Babelなどのトランスパイラを利用します。
webpackなどのモジュールバンドラーの利用も前提としています。
今回は、Parcelを利用しています。

インストール

2019年5月現在、まだまだダウンロード数は多く、元気な様子です。

npm install flux --save

flux | npm

最初はDispatcherしか提供されていませんでしたが、しばらくしてStoreやStore-View間の接続方法を提供するモジュール群が追加されました。
その為かは分かりませんが、各モジュールは下記のように分かれています。

// Dispatcherの利用
import { Dispatcher } from 'flux';

// その他のモジュール
import {
  Store,
  ReduceStore,
  Container
} from 'flux/utils';

Dispatcherの準備

アプリケーションのハブとなる、Dispatcherを用意します。

dispatcher.js
import { Dispatcher } from 'flux';

export default new Dispatcher();

Dispatcher関数を利用して、Dispatcherオブジェクトを生成し、公開します。
これだけです。こちらで何か処理を書くことはありません。

Dispatcher | Flux

ActionとActionCreator

ViewからStoreへ、Dispatcherを通して配信されるActionと、それを実行するActionCreaterを定義します。
また、登録したTodoリストの表示を切り替えるフィルターを定義した定数も作成して公開します。

actions.js
import Dispatcher from './dispatcher';

// 表示フィルターの定数
export const VisibilityFilters = {
  SHOW_ALL: 'SHOW_ALL',
  /* 略 */
};

// アクションの定数
export const TodoActionTypes = {
  ADD_TODO: 'ADD_TODO',
  /* 略 */
};

let nextTodoId = 0;

// アクションクリエイター
export const TodoActions = {
  /**
   * Todo項目の追加
   * @param {String} text Todoテキスト
   */
  addTodo(text) {
    Dispatcher.dispatch({
      type: TodoActionTypes.ADD_TODO,
      id: nextTodoId++,
      text
    });
  },
  /* 略 */
};

アプリ内で共有する、Actionの識別子「ActionType」を定義して公開します。
続いて、Actionを作成してDispatcherに送信する関数(ActionCreator)を定義して公開します。
この関数は、ユーザの入力値を引数に受け取ります。
Todoを追加するActionの項目は、ActionTypeと新規に発行した項目ID、ユーザが入力したテキストです。
Dispatcherオブジェクトのdispatchメソッドを利用してActionを送信し、データと共にStoreに変更の発生を通知します。

Storeの作成

Stateの定義

まず、Todoアプリの状態「State」を表したオブジェクトを考えます。
todoリストを保持する配列todos、リストの表示フィルタの種類を保持するvisibilityFilterがあります。
todosvisibilityFilterで、それぞれ管理を担当するStoreをあえて分けて作成することにします。

{
  todos: [
    {
      id: todoId,
      text: todoText,
      completed: false
    }
  ],
  visibilityFilter: filterType
}

Storeの作成(Todoリスト部分)

Storeの基本機能を備えたStoreまたはStoreを拡張したReduceStoreクラスをインポートし、それを継承したクラスを作成します。
下記は、Todoリストの追加と切替のActionを受付するStoreです。
アプリのStateのtodosの部分をを担当します。
FluxUtilsのReduceStoreクラスを継承して作成しています。

/stores/todosStore.js
import { ReduceStore } from 'flux/utils';
import Dispatcher from '../dispatcher';
import { TodoActionTypes } from '../actions';

class TodosStore extends ReduceStore {
  // Todoリストの初期状態を定義して返す
  getInitialState() {
    return [];
  }
  /**
   * 既存の状態とActionの情報をマージして新しい状態を作成する
   * 担当するActionTypeでない場合は、既存の状態をそのまま返す
   * 
   * @param {Object} state 現在の状態
   * @param {Object} action Dispatcher経由で受け取るaction
   * @returns {Object} 新しい状態
   */
  reduce(state, action) {
    switch (action.type) {
      case TodoActionTypes.ADD_TODO:
        return [
          ...state,
          {
            id: action.id,
            text: action.text,
            completed: false
          }
        ];
      case TodoActionTypes.TOGGLE_TODO:
        return state.map(todo =>
          todo.id === action.id ? { ...todo, completed: !todo.completed } : todo
        );
      default:
        return state;
    }
  }
}

// インスタンス作成時、Dispatcherオブジェクトを渡す
export default new TodosStore(Dispatcher);

ReduceStoreには、下記のメソッドがあります。

  • getState(): 外部から、現在のStateを取得する為のメソッドです。基本的にオーバーライド不要です。
  • getInitialState(): Stateの初期値を提供します。一度しか呼ばれません。
  • reduce(currentState, action): 現在の状態とActionの内容から新しい状態を生成する処理を記述します。必ずサブクラス内で実装する必要があります。このメソッド内では、副作用は認められません。
  • areEqual(startingState, endingState): Action発行前と後で、Stateに変更が発生したかを比較します。基本的にオーバーライド不要です。

reduce(currentState, action)メソッド内でStateの変更を実施するにあたり、currentStateをそのまま変更してはいけません。
reduce()で変更されたStateは、areEqual(startingState, endingState)で、以前のStateと比較されます。
そして異なるオブジェクトであることが確認できると、変更の発生がViewへと通知されます。
この部分におけるソースは下記のようになっています。

FluxReduceStore.js抜粋
// areEqual()メソッド(TypeScript)
areEqual(one: TState, two: TState): boolean {
  return one === two;
}

// 内部メソッド__invokeOnDispatch()抜粋
const startingState = this._state;
const endingState = this.reduce(startingState, action);

if (!this.areEqual(startingState, endingState)) {
  this._state = endingState;
  this.__emitChange(); // 通知
}

オブジェクト型の代入は参照渡しですから、startingStateをそのまま変更した場合、正しく比較が出来ません。

const stateA = { param: 'hoge' };
const stateB = stateA;
stateB.param = 'fuga';
console.log(stateA === stateB); // true

console.log(stateA); // {param: "fuga"}
console.log(stateB); // {param: "fuga"}

Storeの仕様以前に、React自体がStateの変更を確認した上で、必要な部分のみ再描画を実行しています。
reduce()メソッド内では必ず、引数で受け取ったcurrentStateObject​.assign()スプレッド構文immutable.jsなどツールを用いて、Stateを複製した上で変更する必要があります。

同じ要領で、StateのvisibilityFilterの部分を担当するStoreも作成します。

ReduceStore | Flux
オブジェクトの比較 | MDN
はてなブックマーク検索を作りながらFlux Utilsについて学ぶ | Web Scratch

StoreとViewの接続

Todoアプリの状態を管理する2つのStoreと、Viewとの接点となるContainer-Componentを作成します。

containers/app.jsx
import { Container } from 'flux/utils';
// 全てのStore
import TodosStore from '../store/todosStore';
import VisibilityFilterStore from '../store/visibilityFilterStore';

import { TodoActions } from '../actions';
import RootApp from '../components/';

class TodoContainer extends React.Component {
  // 参照するStoreを配列で渡す
  static getStores() {
    return [
      TodosStore,
      VisibilityFilterStore
    ];
  }
  // Viewに提供するStateを生成
  // 各イベントハンドラで実行するActionCreatorも追加
  static calculateState() {
    return {
      todos: TodosStore.getState(),
      visibilityFilter: VisibilityFilterStore.getState(),

      ...TodoActions
    };
  }

  render() {
    return <RootApp {...this.state} />;
  }
}

export default Container.create(TodoContainer);

staticメソッドgetStores()calculateState()をオーバーラップしたコンポーネントを作成します。
getStoresでは、全てのStoreを配列で返すようにします。
calculateStateで返す値は、Container-Componentのthis.stateとなる値です。
各StoreのgetState()メソッドの戻り値と、ActionCreatorを設定します。
そして、renderメソッド内でViewのルートコンポーネントにstateを渡します。
Container-Componentでは描画する要素は定義しません。
定義したClassコンポーネントを、FluxUtilsのContainerオブジェクトのcreateメソッドに渡し、戻り値を公開します。

Container | Flux

View

このようなコンポーネントツリーになっています。

Container-Components
 │
App            # Viewのルートコンポーネント
 ├ AddTodo     # 新規Todo入力フォーム
 ├ TodoList    # 追加されたTodoリスト
 │    └ Todo   # Todo項目
 └ Footer      # 表示リストの切替メニュー
       └ Link  # 切替ボタン

ActionCreatorの利用

AddTodoでは、prop経由で受け取ったActionCreatorを、サブミットイベントのハンドラ内で実行してActionを発行・送信します。

components/addTodo.jsx
const AddTodo = ({ addTodo }) => {

  let input;

  return (
    <div>
      <form
        onSubmit={e => {
          e.preventDefault();
          if (!input.value.trim()) {
            return;
          }
          addTodo(input.value);
          input.value = '';
        }}
      >
        <input ref={node => (input = node)} />
        <button type="submit">Add Todo</button>
      </form>
    </div>
  );
};

イメージ図

自分なりにA4にまとめてみました。
TodoList_Flux.PNG

koedamon
WEBフロント
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away