Carpe Diem

備忘録

React.js+Fluxでcounter

概要

Fluxアーキテクチャを勉強しようと思って簡単なcounterを作ってみました。

github.com

動作ページはこちら

環境

  • React.js 15.0.1
  • Babel 6.5.2
  • webpack 1.12.15

役割

f:id:quoll00:20160712151936p:plain

担当 役割
View 画面の表示。ユーザイベントをActionへ通知
Action アクションタイプ属性を持たせてDispatcherへ通知。外部APIへのリクエストはここ。
Dispatcher Storeへ渡す
Store データの保持や実処理を行う。変更をViewへ通知

ディレクトリ構造

.
├── actions
│   └── action.js
├── components
│   ├── app.jsx
│   ├── count.jsx
│   └── display.jsx
├── counter.jsx
├── dispatcher
│   └── dispatcher.js
└── stores
    └── store.js

実装

action

import CounterDispathcer from '../dispatcher/dispatcher';

var CounterActions = {
  increment: function() {
    CounterDispathcer.dispatch({
      actionType: 'increment'
    });
  },
  decrement: function() {
    CounterDispathcer.dispatch({
      actionType: 'decrement'
    });
  }
}

module.exports = CounterActions;

ポイント

  • アクションタイプを持たせてDispatcherへ送る

dispatcher

var Dispatcher = require('flux').Dispatcher;

module.exports = new Dispatcher();

store

import CounterDispatcher from '../dispatcher/dispatcher';
import Events from 'events';
import assign from 'object-assign';

var EventEmitter = Events.EventEmitter;

var count = {
  value: 0
};

function increment() {
  count.value++;
}

function decrement() {
  count.value--;
}

var CounterStore = assign({}, EventEmitter.prototype, {
  getAll: function() {
    return count;
  },
  emitChange: function() {
    this.emit('change');
  },
  addChangeListener: function(callback) {
    this.on('change', callback);
  },
  removeChangeListener: function() {
    this.removeListener('change', callback);
  }
});

CounterDispatcher.register((action) => {
  switch(action.actionType) {
    case 'increment':
      increment();
      CounterStore.emitChange();
      break;
    case 'decrement':
      decrement();
      CounterStore.emitChange();
      break;
    default:
  }
});

module.exports = CounterStore;

ポイント

  • アクションタイプをswitchで振り分け、処理を実行する
  • Viewへイベントを通知する。
  • Storeにはeventsモジュール必要
  • Storeにはグローバルオブジェクトができる

view

app.jsx

import React from 'react';
import CounterStore from '../stores/store';
import CounterDisplay from './display.jsx';
import CounterCount from './count.jsx';

class CounterApp extends React.Component {
  constructor(props) {
    super(props);
    this.state = CounterStore.getAll();
  }
  componentDidMount() {
    CounterStore.addChangeListener(this._onChange.bind(this));
  }

  componentWillUnmount() {
    CounterStore.removeChangeListener(this._onChange.bind(this));
  }

  _onChange() {
    var v = CounterStore.getAll();
    this.setState(v);
  }

  render() {
    return (
      <div>
        <CounterDisplay data={this.state.value} />
        <CounterCount />
      </div>
    );
  }
}

module.exports = CounterApp;

ポイント

  • Storeからの通知を受け取る形に実装する。
  • getInitialStateは使わずconstructorでthis.state = xxx
  • stateに入れるのはobjectじゃないとだめ
  • .jsxのときはimport時も.jsxを付ける
  • Componentをmodule.exportsするときはclassのまま。newしない
  • thisをbindする必要がある http://stackoverflow.com/questions/33631482/react-this-is-null-in-event-handler

count.jsx

import React from 'react';
import CounterActions from '../actions/action'

class CounterCount extends React.Component {
  render() {
    return (
      <div>
        <button onClick={this._increment}>+1</button>
        <button onClick={this._decrement}>-1</button>
      </div>
    );
  }
  _increment() {
    CounterActions.increment();
  }
  _decrement() {
    CounterActions.decrement();
  }
}

module.exports = CounterCount;

display.jsx

import React from 'react';
var ReactPropTypes = React.PropTypes;

class CounterDisplay extends React.Component {
  constructor(props) {
    super(props);
  }
  render() {
    return (
      <div>
        <h1>{this.props.data}</h1>
      </div>
    );
  }
}

CounterDisplay.propTypes = {
  data: ReactPropTypes.number.isRequired
};

module.exports = CounterDisplay;

ソース