ReactとFluxでクライアントサイドの設計 このエントリをはてなブックマークに登録

2015年11月04日

ダニーダニー / ,

はじめに

こんにちは。皆さんはReact使われてますでしょうか?
今回はReactとFluxについて書きます。

MVCの問題点

詳しくはこちらに書いていますが、MVCでアプリを作った場合は、ModelとViewの間で双方向のデーターフローが作られる可能性があるので理解したりデバックするのが難しくなります。

React単体で使った場合の問題点

子のViewでなにかアクションを起こした結果、親のViewにも変更を伝えてなににかをする場合、子のViewに親のViewのコールバック関数を渡す必要があります。

子のViewの下にさらに、子のViewがあって、アクションが起こった時に親のViewに伝えようとするとコールバックが深くなって複雑になってしまいます。

Flux

それを解決できる方法としてFluxです。
Fluxを使うとデーターフローが一方向になります。
やってることはObserverパターンです。

Fluxを実現するのに使うライブラリ

Fluxの実装自体は自前で実装してもそこまで大変ではないですが、Fluxフレームワークは何種類か出ています。

自分が調べた限りだと、現状はこのフレームワークが凄く使われてて、特にいいというのがなさそうだったので、今回は本家のFacebookのFluxを使います。

React

https://github.com/facebook/react

Viewとして使います。

Flux

https://github.com/facebook/flux

Dispatcherがライブラリとして使えるので使います。

EventEmitter3

https://github.com/primus/eventemitter3

StoreからViewに更新があったことをイベントとして通知するのに使います。

Node.jsの events.EventEmitterと同じように使えます。

EventEmitter2は高速化とブラウザでも使えるようにしたもので、EventEmitter3はさらに高速化したライブラリです。

デモ

http://f96q.github.io/flux

View

src/components/item_form.js

'use strict';

import React from 'react'
import ItemActions from '../actions/item_actions'

export default class ItemForm extends React.Component {
  create(event) {
    let text = this.refs.itemText.getDOMNode().value;
    if (text) {
      ItemActions.create(text);
      this.refs.itemText.getDOMNode().value = '';
    }
  }
  render() {
    return (
      <div className="input-group">
        <input type="text" className="form-control" ref="itemText"></input>
        <span className="input-group-btn">
          <button className="btn btn-primary" type="button" onClick={this.create.bind(this)}>+</button>
        </span>
      </div>
    );
  }
}

まずユーザーがフォームにテキストを入力して、+のボタンを押した時にonClickでItemActionsのcreateが呼ばれます。

Action

src/actions/item_actions.js

'use strict';

import uuid from 'uuid'
import Dispatcher from '../dispatcher/dispatcher'

class ItemActions {
  constructor(dispatcher) {
    this.dispatcher = dispatcher;
  }
  dispatch(params) {
    this.dispatcher.dispatch(params);
  }
  create(body) {
    this.dispatch({
      type: 'create',
      item: {id: uuid.v4(), body: body}
    });
  }
}

export default new ItemActions(Dispatcher);

ItemActionsのcreateはthis.dispatcherにデーターを渡します。
今回の場合はサーバーにデーターを保存しないので直接ですが、APIを叩いてサーバーにデーターを保存する場合は、Ajaxの成功した時のコールバックでthis.dispatcherを呼ぶように使います。

Dispatcher

src/dispatcher/dispatcher.js

'use strict';

import {Dispatcher} from 'flux'

export default new Dispatcher();

Dispatcherの実装はFluxのものを使うので、インスタンスを作るだけですみます。

Store

src/stores/item_store.js


'use strict';

import EventEmitter from 'eventemitter3'
import Dispatcher from '../dispatcher/dispatcher'

class ItemStore {
  constructor(dispatcher) {
    this.state = this.getInitialState();
    this.eventEmitter = new EventEmitter();
    this.dispatcher = dispatcher;
    this.changeEvent = 'change';
    this.dispatcher.register((action) => {
      this.invokeOnDispatch(action);
    });
  }
  getState() {
    return this.state;
  }
  getInitialState() {
    return {items: []};
  }
  addListener(listener) {
    this.eventEmitter.addListener(this.changeEvent, listener);
  }
  removeListener(listener) {
    this.eventEmitter.removeListener(this.changeEvent, listener);
  }
  emitChange() {
    this.eventEmitter.emit(this.changeEvent);
  }
  invokeOnDispatch(action) {
    switch (action.type) {
      case 'create':
        this.state.items.push(action.item);
        this.emitChange();
      break;
    }
  }
}

export default new ItemStore(Dispatcher);

ItemActions.createでdispatchされたデーターを受けます。
invokeOnDispatchの中でcreateの場合はthis.state.itemsにitemを一つ追加してstateが変わったことをthis.emitChange()で通知します。

View

src/components/app.js

'use strict';

import React from 'react'
import ItemStore from '../stores/item_store'
import ItemForm from './item_form'
import Items from './items'

export default class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = ItemStore.getState();
  }
  componentDidMount() {
    ItemStore.addListener(this.onChange.bind(this));
  }
  componentWillUnmount() {
    ItemStore.removeListener(this.onChange.bind(this));
  }
  onChange() {
    this.setState(ItemStore.getState());
  }
  render() {
    return (
      <div className="container">
        <ItemForm />
        <Items items={this.state.items} />
      </div>
    );
  }
}

ItemStoreでemitChangeした時に呼ばれるItemStore.addListenerにonChangeを登録しておきます。

onChangeではthis.setStateでStoreから取得したデーターを元にReactがViewをレンダリングします。

ここまでがユーザが操作した結果をもとに画面の表示が変わる一通りのフローです。
データフローも一方向にできました。

リポジトリ

https://github.com/f96q/react-flux-sample

最後に

今回は説明する都合上、最小のコードをサンプルを載せましたが、実際のWebアプリのサンプルとしては、リモートでふりかえりができるWebサービスのKPTBoardを最近、ReactとFluxで書き換えましたので興味がある方は参照下さい。

https://github.com/f96q/kptboard

宣伝

DocBaseは情報共有を活発にしてチームが力を最大限発揮できるようにと思って作ったサービスです。今回の記事で書いたように試行錯誤を繰り返して作っています。弊社のような開発をする組織には便利に使えるように作っていますので、ぜひ試してみてください。

詳しくはこちらから。
https://docbase.io

053c751a-9a40-43e2-ad6e-b75e78367451

  1. メモからはじめる情報共有 DocBase 無料トライアルを開始
  2. DocBase 資料をダウンロード

「いいね!」で応援よろしくお願いします!

このエントリーに対するコメント

コメントはまだありません。

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)


トラックバック

we use!!Ruby on RailsAmazon Web Services

このページの先頭へ