LoginSignup
156
122

More than 3 years have passed since last update.

今から始めるReact入門 〜 flux編

Last updated at Posted at 2018-09-11

目次

はじめに

この記事は"今から始めるReact入門" 記事の続編です。最初から読む場合は以下の記事からスタートしてください。

flux とは

Flux とはユーザインタフェースを構築するためのアプリケーションアーキテクチャです。

React は状態管理についての手法を持ち合わせておリません。その理由はReact はview レイヤのフレームワークであるからです。
どこに状態に関するデータを保存するかといったことについてはView レイヤは持ち合わせておらず、Store (状態を格納する場所)が状態データの保管について役割を担っており、SPA(Single Page Application)ではこれをWeb ブラウザ等のクライアント側で行ってあげる必要があります。
flux とは、このようなSPA(Single Page Application) を実現するために状態管理の手法までを含めた、フレームワークでは無い、アプリケーションを開発するための設計パターンです。
flux を採用することは、安定したアプリケーションを開発するのにとても有効な手法であり、またflux 自体はあくまで設計パターンなのでReact 以外のflux 思想ベースの他のフレームワークを使用してアプリケーション開発をするといった選択肢も取ることができます。

そもそも状態(管理)とは

状態管理の簡易イメージ

そもそも状態(管理)とは、例えばtwitter のようなWeb アプリケーションをイメージしてもらえるとわかりやすいと思います。

twitter をWeb ブラウザで開いたときに通知が無い場合、最初は未読通知が0 件の状態で表示されます。

未読通知が0件の状態
          +---------------------------+
          ↓                           |
    +-------------+             +-------------+
    | Web browser |             | API         |
    +-------------+             +-------------+
    😑(未読0件)

暫くすると、新しい通知として未読通知が1 件API(サーバ) 側から通知され、未読通知1 件としてWeb ブラウザの画面に表示されます。

未読通知1件追加
                   + 1件
          +---------------------------+
          ↓                           |
    +-------------+             +-------------+
    | Web browser |             | API         |
    +-------------+             +-------------+
      😐(未読1件)

また暫くすると2 件の通知がAPI 側から通知され、未読通知3 件として画面上部に表示されるます。

未読通知2件追加(合計3件)
                   + 2件
          +---------------------------+
          ↓                           |
    +-------------+             +-------------+
    | Web browser |             | API         |
    +-------------+             +-------------+
      🤩(未読3件) <- 未読1 件 + API から通知された未読2 件

ここで注意してほしいのが、2 回目のAPI 側から通知が来た時に、Web ブラウザ側は2 件と表示すること無く、現在まで未読だった1 件も含め合計3 件と表示していることです。
状態管理ができていないと、API 側から2 件の新着未読の通知が送られてきた時、3 件ではなくAPI から送られてきた2 件をそのまま表示してしまうことになります。

これをもう少しJavaScript のコード的に見ていきましょう。

state についてもう少し詳しくプログラミング言語観点で

まず、Redux でいうところのstate とは一言でいうと現在の状態等を表すデータを保管するもののことで、React 自身はJSX を使い画面をレンダリング機能はありますが、状態を保存したり引き出したり変更したりといったことが簡単にできるといった思想で作成されてはいません。
しかし、React にstate そのものが無いわけではなく、みなさんはReact を使ってアプリを作成する時にstate を保持するステートフルなReact コンポーネントを既に書いているはずです。
それはJavascript ES6 で実装されているclass 記法で作成した以下のようなReact コンポーネントです。

stateを保持しているreactcomponentの例
import React, { Component } from "react";

class ExampleComponent extends React.Component {
  constructor() {
    super();

    this.state = {
      unread_notifications: [
        { user: "Taro", message: "Hello" },
        { user: "Jiro", message: "Good afternoon" },
        { user: "Ann", message: "Good evening" }
      ]
    };
  }

  render() {
    const { unread_notifications } = this.state;
    return <ul>{unread_notifications.map(el => <li key={el.name}>{el.message}</li>)}</ul>;
  }
}

React.Component を拡張したclass (React コンポーネント)はそれぞれ自身のstate を持っており、その自身のデータを表示します。
そしてReact ではsetState を使って自身のコンポーネントのstate を更新することができています。
これだけ見ていると状態管理は簡単にできそうに見えるのですが、アプリケーションがもっと複雑になり、例えばAPI 側から受動的にpush 方式で追加で3 件未読通知が来たり、同じページ内の別機能であるダイレクトメッセージの取得ボタンを押してダイレクトメッセージをAPI 側に取得しにいったり…とした場合にページ内のコンポーネントのstate の整合性を保とうとして、コードが複雑になったり保守性が低くなったりと色々な弊害が発生しかねません。

また、state のデータ構造に着目すると、それ自体は次のようなシンプルなオブジェクトになっています。
例えば未読通知が1 件だった場合、初期状態としては例えば以下のように表せます。

未読通知1件の時のstate
this.state = {
  unread_notifications: [
    { user: "Taro", message: "Hello" }
  ]
};

そこに追加で2 件通知がくると、state は次のような状態になります。

未読通知3件の時のstate
this.state = {
  unread_notifications: [
    { user: "Taro", message: "Hello" },
    { user: "Jiro", message: "Good afternoon" },    // <- 追加
    { user: "Ann", message: "Good evening" }        // <- 追加
  ]
};

JavaScript ではこのような変化をどのように追跡すれば良いでしょう?
このstate の追跡を助けてくれるライブラリは無いでしょうか?

SPA が出る前までは定期的にWeb browser 側からアプリケーションサーバ側に未読通知の件数を取得しに行き、アプリケーションサーバ側ではセッションを使ってユーザ毎の累計の未読件数をその都度Web browser 側に通知する…といった動作が一般的でしたが、SPA が出てき始めてからバックグラウンドはシンプルなREST API 等で建てられるようになってきてきました。
しかしその代わりにWeb ブラウザ側で現在の未読件数が何件なのか、状態を管理しておく要求が出てきました。
この要求に対応するための手法が、これから話していくflux の状態管理になります。

flux のイメージ図

flux では状態管理と、その状態のレンダリングの一連の流れを以下のように図示しています。

fluxのイメージ図
                  +-------------+
       +----------|  Actions    |<---------+
       |          +-------------+          |
       |                 .                 |
       |                 .                 |
       |          +-------------+          |
       |          |  Constants  |          |
       |          +-------------+          |
       |                 .                 |
       ▾                 .                 |
+-------------+   +-------------+   +------+------+
| Dispatcher  |-->|  Stores     |-->| View        |
+-------------+   +-------------+   +-------------+

View は画面に当たり、ユーザがクリックするボタンや検索フォームのようなものを備えている想定で、Actions はその動作をListen している状態です。
Actions はサーバ上のAPI のようなところ起動されることもあり、そちらをListen している場合もあります。
例えばユーザがボタンをクリックするとActions に処理が渡され、Dispatcher(場合によってはConstants のようなシンプルな変数の変更処理)に渡った後にStore に結果が格納され、View はStore に変更が合った時にその値で自分自信を新しい情報でレンダリングします。

Actions が発行するデータ

Actions は例えばユーザが画面操作によって発生するデータの更新要求であったり、バックグラウンドから発生するデータの更新要求を発行する部分となり、具体的にAction が発行するデータとしては以下のようなものになります。

Actionのオブジェクトの形式例(TODOアプリで、TODOを追加する場合の発行データの例)
{
  type: "ADD_TODO",
  data: {
    name: "Study English",
    description: "Studing English with Bob"
  }
}

そしてAction からDispatcher へデータはdispatch され、JavaScript のコードで表すと、次のようなイメージになります。

ActionからDispatcherへ命令を発行する時のJavaScript記述例
Dispatcher.dispatch({
  type: "ADD_TODO",
  data: {
    name: "Study English",
    description: "Studing English with Bob"
  }
});

View やAction はDispatcher にdispatch でデータと処理を渡した後、裏側でどんなに複雑な処理をしていようと、それについては意識する必要がありません。

Dispatcher

ベースはpub/sub の思想です。
Action から受け取った処理をDispatcher は複数の接続先、例えばDB や他API サーバ等にアクセスして必要なデータを処理したり、計算処理をする役割を持っています。
ここでDispatcher は1 つのアプリケーションに対して1 つとなり、シングルトンパターンが使われることが多いです。
処理された結果データは随時Stores に送られます。

Stores

Dispatcher が処理した結果を蓄え、View がレンダリングするためのデータを格納する役割を持っています。
Stores は1 つのアプリケーションに複数存在することもありえますが、それぞれがシングルトンパターンとして存在し、JavaScript のコードではAction からのdispatch にコールバックを登録しておき、Dispatcher の処理が完了後にStore 自身のデータが更新されるという流れが一般的です。
Store に蓄えられたものはView にレンダリングされます。

View

View の役割はStores のデータを検知し、そのデータをレンダリングして表示することで、View のインタフェースによってはユーザからのボタン等の操作により、新たなAction を発生させる役割を持っているものもあります。
View はActions やDispatcher がどのような処理をしているかについては気にする必要が無く、Stores の変更を検知してその値をレンダリングすれば良く、バックエンド側と綺麗に切り離されているため分業も簡単になります。
またデータの流れが一方通行になりシンプルになるため、バグの少ないアプリケーション作成にも期待が持てます。

ここで各コンポーネントの概要の説明は完了しました。
ここからはTODO 管理アプリケーションを作成し、flux 思想に沿って作成されていないアプリをflux 思想に沿ったアプリにしていくことでこの思想について勉強していきたいと思います。

ソースコード

プロジェクトの作成

それでは、新しいプロジェクトを作成し構築していきましょう。
flux のチュートリアル開始時点でのソースコードは以下のリポジトリにもあるので、参考になればと思います。

プロジェクトの作成
$ mkdir todolist
$ cd todolist
$ npm init -y
$ npm install --save-dev @babel/core @babel/preset-env @babel/preset-react babel-loader \
        babel-plugin-react-html-attrs babel-plugin-add-module-exports \
        babel-plugin-transform-class-properties babel-plugin-transform-decorators-legacy \
$ npm install --save-dev react react-dom react-router react-router-dom webpack webpack-cli webpack-dev-server

今回は新たにflux パッケージもインストールしておきます。

fluxのインストール
$ npm install --save-dev flux

webpack.config.js を作成し、以下のように記述します。babel-loader のオプションについては.babelrc に記述を移すことも可能です。

webpack.config.js
var debug   = process.env.NODE_ENV !== "production";
var webpack = require('webpack');
var path    = require('path');

module.exports = {
  context: path.join(__dirname, "src"),
  entry: "./js/client.js",
  module: {
    rules: [{
      test: /\.jsx?$/,
      exclude: /(node_modules|bower_components)/,
      use: [{
        loader: 'babel-loader',
        options: {
          plugins: ['react-html-attrs'],
          presets: ['@babel/preset-react', '@babel/preset-env']
        }
      }]
    }]
  },
  output: {
    path: __dirname + "/src/",
    filename: "client.min.js",
    publicPath: '/'
  },
  devServer: {
    historyApiFallback: true
  },
  plugins: debug ? [] : [
    new webpack.optimize.OccurrenceOrderPlugin(),
    new webpack.optimize.UglifyJsPlugin({ mangle: false, sourcemap: false }),
  ],
};

引き続きindex.html やjs を作成していきますが、量が多いため割愛します。
以下のリポジトリにファイル一式が用意されているので、それを使用していきます。

ソースコード構成
todolist
  + package.json
  + webpack.json
  + src/
    + index.html
    + client.min.js    // webpack にて自動生成
    + js/
      + client.js
      + actions/
        + TodoActions.js
      + components/
        + Todo.js
        + layout/
          + Footer.js
          + Nav.js
      + pages/
        + Favorites.js
        + Layout.js
        + Settings.js
        + Todos.js
      + stores/
        + TodoStore.js    // ハンズオンの途中で新規作成します

ここまでできてたら、server を起動して初期画面が表示されることを一旦確認してみましょう。

npmstart
$ npm start

Web ブラウザでhttp://localhost:8080 へアクセスし、トップページが表示されることを確認してください。

  • 初期画面

ReactFlux_React0000.png
ReactFlux_React0001.png

Todo.js に下記のようにView にレンダリングするためのデータが書かれているので、これを先程示したflux 思想に従って適切な場所へと置き換えていきます。

src/js/pages/Todos.js
import React from "react";

import Todo from "../components/Todo";

export default class Todos extends React.Component {
  constructor() {
    super();
    this.state = {
      todos: [
        {
          id: 113464613,
          text: "Go Shopping",
          complete: false
        },
        {
          id: 235684679,
          text: "Pay Bills",
          complete: false
        }
      ]
    };
  }

  render() {
    const { todos } = this.state;

    const TodoComponents = todos.map((todo) => {
      return <Todo key={todo.id} {...todo}/>;
    });

    return (
      <div>
        <h1>Todos</h1>
        <ul>{TodoComponents}</ul>
      </div>
    );
  }
}

Store の作成

状態を格納するStore を作成します。Store としてsrc/js/stores/TodoStore.js を新規作成します。
Store の役割として、Store が保持しているデータに変更が合った場合にView に対してそれを直ちに送る機能持ちます。
その機能を実現するためにEventEmitter を使ってStore を作成していきます。

src/js/stores/TodoStore.js
import { EventEmitter } from "events";

class TodoStore extends EventEmitter {
  /* TODO: */
}

const todoStore = new TodoStore;

export default todoStore;

状態管理をするStore はそれぞれシングルトンパターンとなるように作成していきます。
シングルトンな構成とするために、const todoStore = new TodoStore; とインスタンスを作成してから、そのインスタンスをexport します。
次にTodo.js にあったデータをStore のconstructor に移動し、それを取得するためのgetAll() メソッドも作成します。

src/js/stores/TodoStore.js
 import { EventEmitter } from "events";

 class TodoStore extends EventEmitter {
+  constructor() {
+    super();
+    this.todos = [
+      {
+        id: 113464613,
+        text: "Go Shopping",
+        complete: false
+      },
+      {
+        id: 235684679,
+        text: "Pay Bills",
+        complete: false
+      }
+    ];
+  }
+
+  getAll() {
+    return this.todos;
+  }
 }

 const todoStore = new TodoStore;

 export default todoStore;

デフォルトで2 件のTodos が作成されて表示されるようになっており、まだこの時点ではまだDispatcher からデータを受け取ることは意識していません。

TodoStore へデータを移行したことで、Todo からデータは削除し、TodoStore からデータをインポートする形式へ変えていきます。
以下のTodoStore.getAll() を定義し、Todo リストのデフォルトを表示するようにします。

src/js/pages/Todos.js
 import React from "react";

 import Todo from "../components/Todo";
+import TodoStore from "../stores/TodoStore";

 export default class Todos extends React.Component {
   constructor() {
     super();
     this.state = {
-      todos: [
-        {
-          id: 113464613,
-          text: "Go Shopping",
-          complete: false
-        },
-        {
-          id: 235684679,
-          text: "Pay Bills",
-          complete: false
-        }
-      ]
+      todos: TodoStore.getAll()
     };
   }

   render() {
     const { todos } = this.state;

     const TodoComponents = todos.map((todo) => {
       return <Todo key={todo.id} {...todo}/>;
     });

     return (
       <div>
         <h1>Todos</h1>
         <ul>{TodoComponents}</ul>
       </div>
     );
   }
 }

この時点でWeb 画面を確認してみましょう。
今までと画面の表示は変わりませんが、View はStore からデータを受け取ったTodo リストを表示するように内部的には変更されています。
実際にStore からデータが取得されて表示されていることを確認するために、TodoStores 内のデータを書き換えて表示が切り替わることも確認してみましょう。

src/js/stores/TodoStores.js
 import { EventEmitter } from "events";

 class TodoStore extends EventEmitter {
   constructor() {
     super();
     this.todos = [
       {
         id: 113464613,
         text: "Go Shopping",
         complete: false
       },
       {
         id: 235684679,
-        text: "Pay Bills",
+        text: "Pay Water Bills",
         complete: false
       }
     ];
   }

   getAll() {
     return this.todos;
   }
 }

 const todoStore = new TodoStore;

 export default todoStore;

これでStore の作成は完了です。
次はこの静的にTodo リストが更新されている状態を、もう少し動的に更新されるようにカスタマイズしていきます。

動的にTodo リストが更新されるようにする

これまでのサンプルではソースコードにハードコーディングされたTodo リストをView に渡しているだけでしたが、Dispatcher 作成の前段階として、これをもう少し動的にデータを渡すようにカスタマイズしていきます。
Store にcreateTodo メソッドを作成します。
このcreateTodo メソッドは、呼ばれるとchange イベントを発動し、イベント駆動形でTodo.js (View) の処理を呼び出します。

src/js/stores/TodoStore.js
 import { EventEmitter } from "events";

 class TodoStore extends EventEmitter {
   constructor() {
     super();
     this.todos = [
       {
         id: 113464613,
         text: "Go Shopping",
         complete: false
       },
       {
         id: 235684679,
         text: "Pay Water Bills",
         complete: false
       }
     ];
   }

+  createTodo(text) {
+    const id = Date.now();
+
+    this.todos.push({
+      id,
+      text,
+      complete: false
+    });
+
+    this.emit("change");
+  }
+
   getAll() {
     return this.todos;
   }
 }

 const todoStore = new TodoStore;

 export default todoStore;

次にTodo.js のcomponentDidMount メソッドを追加します。
このメソッドはコンポーネントがマウントされた直後に呼ばれるメソッドで、DOM ノードの初期化処理はこのメソッドに入れることにしましょう。

src/js/pages/Todos.js
 import React from "react";

 import Todo from "../components/Todo";
 import TodoStore from "../stores/TodoStore";

 export default class Todos extends React.Component {
   constructor() {
     super();
     this.state = {
       todos: TodoStore.getAll()
     };
   }

+  componentDidMount() {
+    TodoStore.on("change", () => {
+      this.setState({
+        todos: TodoStore.getAll()
+      });
+    });
+  }
+
   render() {
     const { todos } = this.state;

     const TodoComponents = todos.map((todo) => {
       return <Todo key={todo.id} {...todo}/>;
     });

     return (
       <div>
         <h1>Todos</h1>
         <ul>{TodoComponents}</ul>
       </div>
     );
   }
 }

ではここで動作確認していきますが、debug のためにtodoStore メソッドがグローバルスコープで呼び出せるようにwindow.todoStore にTodoStore インスタンスを格納し、コンソールから呼び出してみることにします。

src/js/stores/TodoStore.js
 import { EventEmitter } from "events";

 class TodoStore extends EventEmitter {
   constructor() {
     super();
     this.todos = [
       {
         id: 113464613,
         text: "Go Shopping",
         complete: false
       },
       {
         id: 235684679,
         text: "Pay Water Bills",
         complete: false
       }
     ];
   }

   createTodo(text) {
     const id = Date.now();

     this.todos.push({
       id,
       text,
       complete: false
     });

     this.emit("change");
   }

   getAll() {
     return this.todos;
   }
 }

 const todoStore = new TodoStore;
-
+window.todoStore = todoStore;
 export default todoStore;

ここでserver を起動してWeb ブラウザを開き画面を確認してみましょう。
F12 キーを押下して開発者モードでconsole を開いてください。
console にてcreateTodo メソッドを打ち込んで新しいTodo を登録してみましょう。

すると画面に新しいTodo (Foo todo)が登録されました。
このプログラムは既にStore のデータが変更されるとView のTodo リストも動的に変更される仕組みが出来上がっています。

次のレッスンではこの仕組みをDispatcher 経由で呼び出すように変更していこうと思います。
(試験目的でグローバル化させたwindow.todoStore = todoStore; の記述は、この時点で削除しておいてください)

Dispatcher の作成

Dispatcher を作成していきます。
ここでついに、環境準備の時にインストールしたflux パッケージを利用していきます。
それではsrc/js/dispatcher.js ファイルを新規作成し、Dispatcher を作成しましょう。

src/js/dispatcher.js
import { Dispatcher } from "flux";

export default new Dispatcher;

Dispatcher の記述はこれだけでOK です。
次にTodoStore.js からこれをimport して、すべてのListener から利用されるhandleActionsメソッドをdispatcher に登録します。

src/js/store/TodoStore.js
 import { EventEmitter } from "events";

+import dispatcher from "../dispatcher";
+
 class TodoStore extends EventEmitter {
   constructor() {
     super();
     this.todos = [
       {
         id: 113464613,
         text: "Go Shopping",
         complete: false
       },
       {
         id: 235684679,
         text: "Pay Water Bills",
         complete: false
       }
     ];
   }

   createTodo(text) {
     const id = Date.now();

     this.todos.push({
       id,
       text,
       complete: false
     });

     this.emit("change");
   }

   getAll() {
     return this.todos;
   }
+
+  handleActions(action) {
+    console.log("TodoStore received an action", action);
+  }
 }

 const todoStore = new TodoStore;
+dispatcher.register(todoStore.handleActions.bind(todoStore));

 export default todoStore;

この時点では、まず動作確認をするためにhandleActions メソッドの内容はデバッグプリントする処理のみとなっています。
dispatcher では主に使うメソッドとして、2 つあり、新たにListener を追加するdispatcher.register とAction に対してデータを創出するdispatcher.dispatch になります。

ここでまた動作確認のため、dispatcher を一時的にグローバル化してGoogle Chrome のコンソールから挙動を確認してみましょう。

src/js/store/TodoStore.js
 import { EventEmitter } from "events";

 import dispatcher from "../dispatcher";

 class TodoStore extends EventEmitter {
   constructor() {
     super();
     this.todos = [
       {
         id: 113464613,
         text: "Go Shopping",
         complete: false
       },
       {
         id: 235684679,
         text: "Pay Water Bills",
         complete: false
       }
     ];
   }

   createTodo(text) {
     const id = Date.now();

     this.todos.push({
       id,
       text,
       complete: false
     });

     this.emit("change");
   }

   getAll() {
     return this.todos;
   }

   handleActions(action) {
     console.log("TodoStore received an action", action);
   }
 }

 const todoStore = new TodoStore;
 dispatcher.register(todoStore.handleActions.bind(todoStore));
-
+window.dispatcher = dispatcher;
 export default todoStore;

Google Chrome からConsole を使ってdispatcher を実行してみましょう。

console
dispatcher.dispatch({type: "some event"});

ReactFlux_React0020.gif

dispatcher が呼び出されることによって自動的にTodoStore のhandleActions が呼び出されて、出力結果よりデータが渡っていることがわかります。
ここから我々が実施することとしては、handleActions に渡ってきたデータをaction type 毎に処理をハンドリングするように条件分岐を書いていけば良いだけです。
action type としてCREATE_TODO を受け取ってTODO を更新するように処理を書いてみましょう。

src/js/store/TodoStore.js
 import { EventEmitter } from "events";

 import dispatcher from "../dispatcher";

 class TodoStore extends EventEmitter {
   constructor() {
     super();
     this.todos = [
       {
         id: 113464613,
         text: "Go Shopping",
         complete: false
       },
       {
         id: 235684679,
         text: "Pay Water Bills",
         complete: false
       }
     ];
   }

   createTodo(text) {
     const id = Date.now();

     this.todos.push({
       id,
       text,
       complete: false
     });

     this.emit("change");
   }

   getAll() {
     return this.todos;
   }

   handleActions(action) {
-    console.log("TodoStore received an action", action);
+    switch(action.type) {
+      case "CREATE_TODO": {
+        this.createTodo(action.text);
+      }
+    }
   }
 }

 const todoStore = new TodoStore;
 dispatcher.register(todoStore.handleActions.bind(todoStore));
 window.dispatcher = dispatcher;
 export default todoStore;

書き終えたらもう一度、Web ブラウザのコンソールからdispatcher を呼び出します。

dispatcher.dispatch({type: "CREATE_TODO", text: "new todo"});

ReactFlux_React0021.gif

するとTodo リストに新しいTodo が登録されました。これでDispatcher は完成です。
次はAction の作成していきましょう。

Actions の作成

Stores, Dispatcher と作成したら最後にActions を作成します。
Action がやることは単にdispatch することだけで、これからTodo を作成するcreateTodo、Todo を削除するdeleteTodo メソッドを作成していきます。
それぞれをオブジェクト内に定義して、それをexport キーワードを使ってexport することも可能ですが、ここではES6 の記法を使って、よりシンプルにfunction をexport していきます。

src/js/actions/TodoActions.js
import dispatcher from "../dispatcher";

export function createTodo(text) {
  dispatcher.dispatch({
    type: "CREATE_TODO",
    text
  });
}

export function deleteTodo(id) {
  dispatcher.dispatch({
    type: "DELETE_TODO",
    id
  });
}
補足
......
export default {
  createTodo: function() {
    /* Do stuff... */
  }
}
......

ここでもう一度flux の図を思い出してください。

fluxのイメージ図
           +-------------+
      ...--|  Actions    |<---------+
           +-------------+          |
                  .                 |
                  .                 |
                                    |
                                    |
                                    |
                  .                 |
                  .                 |
           +-------------+   +------+------+
     ...-->|  Stores     |-->| View        |
           +-------------+   +-------------+

Components(View) はStores とActions とつながっているので、それを意識してComponents にActions を取り込みます。
具体的にいうと、Actions であるTodoActions.js をComponents であるTodo.js 側でimport して、Todo を作成するためのAction を発行するボタンを追加します。

src/js/pages/Todos.js
 import React from "react";

 import Todo from "../components/Todo";
+import * as TodoActions from "../actions/TodoActions";
 import TodoStore from "../stores/TodoStore";

 export default class Todos extends React.Component {
   constructor() {
     super();
     this.state = {
       todos: TodoStore.getAll()
     };
   }

   componentDidMount() {
     TodoStore.on("change", () => {
       this.setState({
         todos: TodoStore.getAll()
       });
     });
   }

+  createTodo() {
+    TodoActions.createTodo("New Todo");
+  }
+
   render() {
     const { todos } = this.state;

     const TodoComponents = todos.map((todo) => {
       return <Todo key={todo.id} {...todo}/>;
     });

     return (
       <div>
+        <button onClick={this.createTodo.bind(this)}>Create!</button>
         <h1>Todos</h1>
         <ul>{TodoComponents}</ul>
       </div>
     );
   }
 }

上記ソースコードの補足ですが、import に* 記号を使うことで、TodoActions.js 内のすべての要素を一度にimport することができます。
今回の場合はimport * as TodoActions from "../actions/TodoActions"; と記述しているので、createTodo, deleteTodo の2 つのfunction がimport されます。

一段落ついたので、ここでWeb ブラウザで動きを確認してみましょう。
"Create!" ボタンをクリックすると新しいTodo が登録されていきます。

ReactFlux_React0030.gif

これでflux 思想に沿ったTodo アプリケーションが完成しました。
ここまでくればあとは自由にカスタマイズしてinput エリアからデータを読み込んで新しいTodo を登録するようにしたり、ID を指定してTodo を削除する機能を追加したりすることも、flux 思想に沿ったやり方でできるようになっています。

次はflux で非同期処理を行う場合はどうするかについて勉強していきます。。

flux で非同期処理を扱う場合

flux で非同期処理を取り扱う場合について勉強していきたいと思います。
例えば新しいTodo を取得する時にインターネット経由でREST API を利用して、非同期にデータを取得して画面をレンダリングする場合の処理について、実際に実装して学んでいきたいと思います。
ここでは先程作成したCreate ボタンを変更して、Todo リストをReload するボタンを実装して、インターネット経由でTodo リストを取得するアプリ作成を通して勉強していきましょう。

src/js/pages/Todos.js
 import React from "react";

 import Todo from "../components/Todo";
 import * as TodoActions from "../actions/TodoActions";
 import TodoStore from "../stores/TodoStore";

 export default class Todos extends React.Component {
   constructor() {
     super();
     this.state = {
       todos: TodoStore.getAll()
     };
   }

   componentDidlMount() {
     TodoStore.on("change", () => {
       this.setState({
         todos: TodoStore.getAll()
       });
     });
   }

-  createTodo() {
-    TodoActions.createTodo("New Todo");
+  reloadTodos() {
+    TodoActions.reloadTodos();
   }

   render() {
     const { todos } = this.state;

     const TodoComponents = todos.map((todo) => {
       return <Todo key={todo.id} {...todo}/>;
     });

     return (
       <div>
-        <button onClick={this.createTodo.bind(this)}>Create!</button>
+        <button onClick={this.reloadTodos.bind(this)}>Reload!</button>
         <h1>Todos</h1>
         <ul>{TodoComponents}</ul>
       </div>
     );
   }
 }

TodoActions.js にreloadTodos メソッドを追加します。
この中に非同期な処理を追記していきますが、具体例にはaxios を使って非同期でどこかのインターネットサイトから情報を引っ張って来るような処理が想像しやすいと思います。
しかし今回は適当な外部サービスが無いので、このメソッドの中で新たにFETCH_TODOS type のアクションを作成し、setTimeout で擬似的に処理を数秒遅らて非同期な処理を実装していきたいと思います。

src/js/actions/TodoActions.js
 import dispatcher from "../dispatcher";

 export function createTodo(text) {
   dispatcher.dispatch({
     type: "CREATE_TODO",
     text
   });
 }

 export function deleteTodo(id) {
   dispatcher.dispatch({
     type: "DELETE_TODO",
     id
   });
 }
+
+export function reloadTodos() {
+  // axios("http://someurl.com/somedataendpoint").then((data) => {
+  //   console.log("got the data!", data);
+  // });
+
+  dispatcher.dispatch({type: "FETCH_TODOS"});
+  setTimeout(() => {
+    dispatcher.dispatch({type: "RECEIVE_TODOS", todos: [
+      {
+        id: 213464613,
+        text: "Go Shopping Again",
+        complete: false
+      },
+      {
+        id: 335684679,
+        text: "Sleep At The Yard.",
+        complete: true
+      }
+    ]});
+  }, 1000);
+}

サンプルでは上記のようになりますが、実際のコードではdispatcher.dispatch が失敗したときはFETCH_TODOS_ERROR を送るように組んでおくとより良いでしょう。
次はTodoStore.js にRECEIVE_TODOS を受け取った時にTodo を更新する処理を追加します。

src/js/stores/TodoStore.js
 import { EventEmitter } from "events";

 import dispatcher from "../dispatcher";

 class TodoStore extends EventEmitter {
   constructor() {
     super();
     this.todos = [
       {
         id: 113464613,
         text: "Go Shopping",
         complete: false
       },
       {
         id: 235684679,
         text: "Pay Water Bills",
         complete: false
       }
     ];
   }

   createTodo(text) {
     const id = Date.now();

     this.todos.push({
       id,
       text,
       complete: false
     });

     this.emit("change");
   }

+  receiveTodos(todos) {
+    this.todos = todos;
+    this.emit("change");
+  }
+
   getAll() {
     return this.todos;
   }

   handleActions(action) {
     switch(action.type) {
       case "CREATE_TODO": {
         this.createTodo(action.text);
       }
+      case "RECEIVE_TODOS": {
+        this.receiveTodos(action.todos);
+      }
     }
   }
 }

 const todoStore = new TodoStore;
 dispatcher.register(todoStore.handleActions.bind(todoStore));
 window.dispatcher = dispatcher;
 export default todoStore;

これで非同期処理の実装が完了です。
Reload! ボタンを押してみましょう。
ReactFlux_React0040.gif
ボタンを押下してから約1 秒後に画面の表示が切り替わりました!
このようにflux 思想に沿って開発を進めていけば非同期なデータ更新も難なく実装することができるようになるのです。

メモリリーク

イベントやリスナのunbind 忘れや失敗により使われないメモリがどんどん溜まっていき、メモリリークが発生することがあります。
JavaScript ではオブジェクトをメモリに蓄え、処理を行うのでメモリリークが起きていることに気づかないまま画面操作を続けていると、突然処理が極端に遅くなったりクラッシュしたり、想定しない動作をしてしまったりといったことが発生することがあります。
まずここでは、メモリリークがどのようなものなのか、実際にメモリリークを発生させてみましょう。

Todo コンポーネントは新しいTodo を表示するたびに新しくレンダリングされ、コンポーネントの分だけメモリが消費されます。

Todo.js のcomponentDidMount にListener を追加する処理を記述しましたが、このメソッドはコンポーネントがレンダリングされるたびに呼ばれるメソッドになり、これは例えば画面上部のナビゲーションメニューからTodo を押下してTodo ページを表示するたびにListener が登録され、何度か繰り返すと過去に登録されたListener が残されたまま、更に新しいListener が登録されることになります。
結果として、前に登録していたListener は開放されること無くメモリ上にどんどん蓄積されていることになります。

それを実際に確認するために、Todos コンポーネントに現在のListenr 数をカウントするデバッグ出力を追加してみましょう。

src/js/pages/Todos.js
 import React from "react";

 import Todo from "../components/Todo";
 import * as TodoActions from "../actions/TodoActions";
 import TodoStore from "../stores/TodoStore";

 export default class Todos extends React.Component {
   constructor() {
     super();
     this.state = {
       todos: TodoStore.getAll()
     };
   }

   componentDidMount() {
     TodoStore.on("change", () => {
       this.setState({
         todos: TodoStore.getAll()
       });
     });
+    console.log("count", TodoStore.listenerCount("change"));
   }

   reloadTodos() {
     TodoActions.reloadTodos();
   }

   render() {
     const { todos } = this.state;

     const TodoComponents = todos.map((todo) => {
       return <Todo key={todo.id} {...todo}/>;
     });

     return (
       <div>
         <button onClick={this.reloadTodos.bind(this)}>Reload!</button>
         <h1>Todos</h1>
         <ul>{TodoComponents}</ul>
       </div>
     );
   }
 }

上記のように修正が完了したら、Web ブラウザのコンソールを開き、何度かTodos メニューを表示させて見ましょう。
するとListener カウントが徐々に増えていき、そのあとにReload! ボタンをクリックしてみると警告が表示されるます。

ReactFlux_React0050.gif

Warning: Can't call setState (or forceUpdate) on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in the componentWillUnmount method.

これはunmount されていないコンポーネントのメソッドが呼ばれようとしたことにより出るエラーです。
これを解消するにはcomponentWillUnmount メソッドで使わなくなったListener をunmount してあげる方法があります。

src/js/pages/Todos.js
 import React from "react";

 import Todo from "../components/Todo";
 import * as TodoActions from "../actions/TodoActions";
 import TodoStore from "../stores/TodoStore";

 export default class Todos extends React.Component {
   constructor() {
     super();
+    this.getTodos = this.getTodos.bind(this);
     this.state = {
       todos: TodoStore.getAll()
     };
   }

   componentDidMount() {
-    TodoStore.on("change", () => {
-      this.setState({
-        todos: TodoStore.getAll()
-      });
-    });
+    TodoStore.on("change", this.getTodos);
     console.log("count", TodoStore.listenerCount("change"));
   }
+
+  componentWillUnmount() {
+    TodoStore.removeListener("change", this.getTodos);
+  }
+
+  getTodos() {
+    this.setState({
+      todos: TodoStore.getAll()
+    });
+   }

   reloadTodos() {
     TodoActions.reloadTodos();
   }

   render() {
     const { todos } = this.state;

     const TodoComponents = todos.map((todo) => {
       return <Todo key={todo.id} {...todo}/>;
     });

     return (
       <div>
         <button onClick={this.reloadTodos.bind(this)}>Reload!</button>
         <h1>Todos</h1>
         <ul>{TodoComponents}</ul>
       </div>
     );
   }
 }

書き終わったらブラウザを開き、先ほどと同じ操作を行ってみましょう。
するとListener カウントは1 のままであることが確認でき、Reload! ボタンをクリックしても先程の警告が出なくなっています。

ReactFlux_React0060.gif

このようにListener を使うときはコンポーネントを使わなくなった時に、都度Listener を削除してあげるようにしましょう。

参考

156
122
7

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
156
122