36
33

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

今からはじめるReact.js〜Flux〜

Last updated at Posted at 2015-10-30

前回→今からはじめるReact.js〜画面遷移〜

処理の流れは一方向で

Flux

React.jsの開発元のFacebookがFluxというアーキテクチャ概念を生み出しました。
アーキテクチャといえばMVC(Model、View、Controler)が有名です。
MVC.png

FluxはMVCで発生する処理の役割を明確化して流れをわかりやすく、シンプルにしたもののようです。

本家サイト( https://facebook.github.io/flux/docs/overview.html )でも図が提示されていますが、

頭の中を整理してみたらこんな感じになりました。
Flux.png

図を見てお分かりの通り、一方通行になっています。
一方通行とすることで、例えばViewはViewの役割に集中できるであるとか、ソースが複雑化するのを防ぐメリットがあるらしいです。

本家サイトの図だと、WEB APIはActionで呼ぶとなっているのですが、特にこれじゃなきゃダメ、というわけではないようです。
データをPOSTするときはその前にバリデーションかけて、NGだったらPOSTしない、とかのフローを考えるとStoreでPOSTしたほうが都合が良い、じゃあActionで呼ぶのはGETしかないかなー、だったら、Storeに統一しよう!ということで、WEB APIは全てStoreから呼ぶことで整理しました。
ただし、本来Store自体は状態の保持しかしないシンプルなものになるべきらしいので、Store自体に直接あれこれゴリゴリ書くのは宜しくないようです。

なお、React.jsの場合、コンポーネントの中でイベントの定義やらhtmlの定義やらを行いますので、本格的なサービスを実装する場合は、Fluxを導入することでViewであるjsxファイルの記述量が結果的に減るという開発者的なメリットもあります。

redux( https://github.com/rackt/redux )など、
fluxアーキテクチャのライブラリはいろいろ出ているみたいですので、基本が分かったら、他のも実践してみると良いかもしれません。

実践

とりあえず基本を実践、ということで一番シンプルなfluxライブラリと、そのほか必要なライブラリも一緒にインストールをします。
$ npm install flux object-assign --save

今回はユーザーリスト画面をfluxに対応させてみます。
修正するソースはuserbox.jsxになります。

userbox.jsxで発生する主要なイベントは、

  • ユーザー一覧の表示(UserBox.getUsers)
  • ユーザー登録(UserForm.handleSubmit、UserBox.handleAddUser)

になります。この2つのイベントをfluxに対応させてみましょう。

Action

View(Reactコンポーネント)でイベントが発生した際に、まず実行されるのがActionクラスになります。Actionクラスで実行したい処理名を指定します。

client/scripts/actions/以下に、
userActions.jsというファイルを作成します。

userActions.js
var Dispatcher = require('../dispatchers/dispatcher');

var userActions = {
  //ユーザー一覧の取得
  load: function(target){
    Dispatcher.handleServerAction({
      type: 'load',
      target: target
    });
  },
  //ユーザーの登録
  register: function (target) {
    Dispatcher.handleServerAction({
      type: 'register',
      target: target
    });
  }
};

module.exports = userActions;

一行目に書いてあるDispatcherのインポートは次で説明します。

loadUsersとregisterが定義されています。で、それぞれでDispatcherのイベントを呼んでいます。
typeにはStoreクラスで実行したい処理を指定します。
targetにはStoreクラスに渡したい引数を指定します。

Dispatcher

次にDispatcherクラスを作成しましょう。

client/scripts/dispatchers/以下に、
dispatcher.jsというファイルを作成します。

dispatcher.js
var Dispatcher = require('flux').Dispatcher;
 
var assign = require('object-assign');
 
var appDispatcher = assign(new Dispatcher(), {
    handleServerAction: function (action) {
        this.dispatch({
            source: 'server',
            action: action
        })
    },
    handleViewAction: function (action) {
        this.dispatch({
            source: 'view',
            action: action
        });
    }
});
 
module.exports = appDispatcher;

object-assignは複数のobjectを結合するためのライブラリです。
今回でいうと、fluxライブラリのDispatcherに、新たに定義するhandleServerAction、handleViewActionを含ませてあげることで、ActionクラスからStoreへdispatchできるようにしている、ということですね。

Dispatcherの役割はさまざまなActionを目的のStoreへ伝播することなので、基本的に上の一ファイルしか作成する必要がありません。今回はクライアントサイドだけで完結する処理と、サーバー側でなんらかが行われる処理とを見た目で区別できるように2つ定義しました。

Store

次にStoreクラスを作成します。

client/scripts/stores/以下に、
userStore.jsというファイルを作成します。

userStore.js
var React = require('react');
var ajax = require('./storeUtils').ajax;

var Dispatcher = require('../dispatchers/dispatcher');
var EventEmitter = require('events').EventEmitter;
var assign = require('object-assign');

var userStore = assign({}, EventEmitter.prototype, {
  data: {userData: []},
  addLoadListener: function (callback) {
    this.on('load', callback);
  },
  removeLoadListener: function (callback) {
    this.removeListener('load', callback);
  },
  addRegisterListener: function (callback) {
    this.on('register', callback);
  },
  removeRegisterListener: function (callback) {
    this.removeListener('register', callback);
  },
  getAjaxResult: function(){
    return userStore.data;
  }
});

まずuserStoreそのものの定義をしています。
EventEmitterはあるイベントが発生したら、別のイベントを実行したい、という時に使えるライブラリです。
「別のイベント」を実行するには、emit()を実行することにより実現するようになっているため、条件によっては「別のイベント」を実行させない、ということもできます。

EventEmitterについては、以下が参考になりました。
http://befool.co.jp/blog/chainzhang/using-node-event-emmiter/

StoreからViewに結果を返すには、EventEmitterで紐付けたコールバックイベントを使うようになっています。addLoadListenerの、
this.on('load', callback);
は、
loadイベントとcallbackファンクションを紐付ける、ということですが、callbackファンクションはViewから指定することになっています。ここはまた後で触れます。
removeLoadListenerは、逆に紐付けを解除します。

getAjaxResultはView側から状態(ユーザー一覧)を取得できるように定義しておきます。

次にバリデーションメソッドです。

userStore.js
//バリデーション
var validation = function(target){
  if (!target.name){
    alert('名前を入力してください');
    return false;
  }
  if (!target.mail){
    alert('メールアドレスを入力してください');
    return false;
  }
  return true;
};

これはPOST前の入力チェックについて記述しています。入力エラーがあったらfalseを返します。

最後に、Dispatcherから伝播されてきたイベント処理を実際に行うための定義を記述します。

userStore.js
userStore.dispatchToken = Dispatcher.register(function (payload) {
  var registerCallback = function(err, res){
    return callback(err, res, 'register');
  }
  var loadCallback = function(err, res){
    return callback(err, res, 'load');
  }
  var callback = function(err, res, name){
    if (err){
      alert(res.text);
      return;
    }
    userStore.data = {userData: JSON.parse(res.text)};
    userStore.emit(name);
  }.bind(userStore);

  var actions = {
    load: function (payload) {
      //ajax通信する
      ajax.get("/get_users", {}, loadCallback);
    },
    register: function (payload) {
      if (!validation(payload.action.target)){
        return;
      }
      //ajax通信する
      ajax.post("/post_user", payload.action.target, registerCallback);
    }
  };

  actions[payload.action.type] && actions[payload.action.type](payload);
});

module.exports = userStore;

Actionクラスの各メソッドで指定した、typeに対応したメソッドを実行するように記述しています。例えば、Actionの、

userActions.js
  load: function(target){
    Dispatcher.handleServerAction({
      type: 'load',
      target: target
    });
  },

は、Storeの

userStore.js
    load: function (payload) {
      //ajax通信する
      ajax.get("/get_users", {}, loadCallback);
    },

に対応していることになります。

なお、

  var callback = function(err, res, name){
    if (err){
      alert(res.text);
      return;
    }
    userStore.data = {userData: JSON.parse(res.text)};
    userStore.emit(name);
  }.bind(userStore);

のcallbackについてですが、サーバ通信の結果、エラーがあった場合はメッセージ表示の後、returnされます。EventEmitterはemit()を実行することにより、紐付けたイベントが実行されますので、上記の場合、エラーだった場合はnameのイベントに紐付けたイベントは実行されないことになります。

同様に、registerのvalidationの結果がfalseだった場合も、emit()は実行されることがないため、registerに紐付けたイベントは実行されません。

ajaxの部分は通信部分を共通化して切り出しただけですので、割愛します。

View

最後にViewである、userbox.jsxを修正します。

修正する箇所は、主に、
・ユーザー一覧データの取得
・ユーザー登録
に関してでした。

まずは必要なライブラリとして、userActions.js、userStore.jsがあります。

userbox.jsx
var React = require('react');
var ReactDOM = require('react-dom');

var UserActions = require('../actions/userActions');
var UserStore = require('../stores/userStore');

サーバー通信はStoreクラスに持って行ったのでsuperagentはここでは不要になりました。

次に、Storeで保持している状態(ユーザー一覧)を取得するメソッドgetUserStoreStatesを定義し、userBox作成時(getInitialState)、ユーザーリストの表示更新時(onViewUsers)に呼ばれるようにします。

userbox.jsx
//Storeで保持している状態を取得する
var getUserStoreStates = function(){
  return UserStore.getAjaxResult();
};

//フォームとリストを一つにしたもの
var UserBox = React.createClass({
  getInitialState:function(){
    return getUserStoreStates();
  },


  onViewUsers:function(){
    //ユーザー一覧データをセット
    this.setState(getUserStoreStates());
  },
  onUpdatedUser:function(){
    //更新が成功したらクリアする
    ReactDOM.findDOMNode(this.refs.userform.refs.name).value = "";
    ReactDOM.findDOMNode(this.refs.userform.refs.mail).value = "";
    this.onViewUsers();
  },

ユーザーの登録が成功した際に実行される処理としてonUpdatedUserを記述します。

次にStoreで定義したイベントと上記のイベントを紐付けます。EventEmitterの件です。

userbox.jsx
  componentWillMount:function(){
    UserStore.addLoadListener(this.onViewUsers);
    UserStore.addRegisterListener(this.onUpdatedUser);
  },
  componentWillUnmount:function(){
    UserStore.removeLoadListener(this.onViewUsers);
    UserStore.removeRegisterListener(this.onUpdatedUser);
  },

これで、Store側でemit(name)を実行すると、nameに応じたイベントが実行されるようになります。

次に、UserFormで追加ボタンをクリックした時の処理、UserBoxが画面表示された時の処理についてですが、UserActionsから目的のイベントがDispatcherに渡されるように、対応したメソッドを実行します。

userbox.jsx
  handleAddUser:function(name, mail){
    //ユーザーを登録
    UserActions.register({name: name, mail: mail});
  },
  componentDidMount:function(){
    //ユーザー一覧データを取得
    UserActions.load();
  },
  render:function(){
    return(
      <div style={{width:"300px"}}>
        <UserForm addUser={this.handleAddUser} ref="userform"/>
        <hr/>
        <UserList userData={this.state.userData}/>
      </div>
    );
  }
});

onUpdatedUserでUserFormの名前やメールアドレスをクリアしたいため、UserFormにrefを追記しました。

UserList、Userについては変更ありません。
UserFormについても、handleSubmitの内容が若干変わった以外は変更ありません。

userbox.jsx
//ユーザーの入力フォームを定義
var UserForm = React.createClass({
  propTypes:{
    addUser:React.PropTypes.func.isRequired
  },
  handleSubmit:function(){
    var name = ReactDOM.findDOMNode(this.refs.name).value.trim();
    var mail = ReactDOM.findDOMNode(this.refs.mail).value.trim();
    this.props.addUser(name, mail);
  },

バリデーションの部分とデータクリアの部分が不要になりました。

以上により、ViewではActionとStoreを利用することでサーバ通信に関する記述やデータの状態をチェックする記述がなくなり、主にデータの表示についてだけ記述すれば良くなったので若干スッキリしました。

サンプルソース

次回→今からはじめるReact.js〜Herokuへのデプロイ〜

36
33
0

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
36
33

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?