はじめてのfacebook/flux

前回書いたReact.jsのコードをfacebook/fluxで書きなおしてみます。前回同様、ほぼ殴り書きです。

Fluxの考え方

基本、React.jsの担当するViewコンポーネントでは状態の変化するデータや、データを変更する処理を持たせたくない。 Fluxを適用してない前回のコードでは仕方なく、ルートコンポーネントでそれらをしてたが、Fluxを適用することで、それらをViewコンポーネントの外側でやるようにする。

fluxの図

データの管理やその操作はStoreでやる。ViewはAction(という名前のパラメータみたいなもの)をDispatcherを通じてStoreに渡し、StoreはそのActionを受けてデータの操作をする。

リポジトリは前回と同じ場所でこちら。

一番簡単なfacebook/flux

前回やった単純にメッセージを表示するだけのReact.jsをFlux化してみる。表示してオシマイなのでActionはない。

/05/message.jsx

import React from "react";

export default class Message extends React.Component {
  render() {
    const { text } = this.props;
    return <div>{text}</div>;
  }
}

前回のコードと変化なし。

/05/app.jsx

import React from "react";
import Message from "./message.jsx";
import MessageStore from "./stores/MessageStore";
import { Container } from 'flux/utils';

class App extends React.Component{

  render (){
    const { message } = this.state;
    return (
      <div>
        <h1>一番簡単なFlux</h1>
        <Message text={message}/>
      </div>
    );
  }
}

App.getStores = () => {
  return [ MessageStore ];
};

App.calculateState = (_prevState) => {
  return {
    message: MessageStore.getMessage()
  };
};

const app = Container.create(App);

export default app;

facebook/flux郡とReactとのつなぎとなるルートコンポーネントはこんな感じで定義する。 App.getStoresで利用するStoreを、App.calculateStateで参照するStoreのデータの取得処理を定義する。

/05/dispatcher/Dispatcher.js

import { Dispatcher } from 'flux';
export default (new Dispatcher());

インスタンス化して定義。

/05/stores/MessageStore.js

import { ReduceStore } from 'flux/utils';
import Dispatcher from '../dispatcher/Dispatcher';

class MessageStore extends ReduceStore {
  getInitialState() {
    return { message: 'おす、おらFlux' };
  }

  getMessage() {
    return this.getState().message;
  }

}
export default (new MessageStore(Dispatcher));

Storeの定義。今回はアクションがないので初期値とゲッターのみを定義。

その他のコードは前回同様。これで以下のように表示される。

一番簡単なflux

アクションを定義する

ボタン押したらメッセージを変更するというアクションを追加してみる。

/06/message.jsx

import React from "react";

export default class Message extends React.Component {
  render() {
    const {
      text,
      onClick
    } = this.props;
    return (
      <div>
        <div>{text}</div>
        <button onClick={onClick}>UPDATE</button>
      </div>
    );
  }
}

UPDATEボタン押したら、propsで受け取った関数を実行するようにするのみ。

/06/app.jsx

import React from "react";
import { Container } from 'flux/utils';
import ActionCreator from "./action/ActionCreator";
import Message from "./message.jsx";
import MessageStore from "./stores/MessageStore";

class App extends React.Component{
  render (){
    const { message } = this.state;
    return (
      <div>
        <h1>一番簡単なFlux</h1>
        <Message text={message} onClick={ActionCreator.updateMessage}/>
      </div>
    );
  }
}

App.getStores = () => {
  return [ MessageStore ];
};

App.calculateState = (_prevState) => {
  return {
    message: MessageStore.getMessage()
  };
};

const app = Container.create(App);

export default app;

messageのpropsにUPDATEボタンクリック時に実行する処理「ActionCreator.updateMessage」を渡す。

/06/action/ActionCreator.js

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

class ActionCreator {}

ActionCreator.updateMessage = () => {
  const d = new Date();
  const message = 'やっほー、' + d.getHours() + '時 ' + d.getMinutes() + '分 ' + d.getSeconds() + '秒 です。';
  Dispatcher.dispatch({
    type:  'UPDATE_MESSAGE',
    message: message
  });
};

export default ActionCreator;

ActionCreatorは単にActionを束ねた名前空間みたいなもの。Dispatcher.dispatchを利用しデータを操作するためのリクエストをStoreに渡す。

/06/stores/MessageStore.js

import { ReduceStore } from 'flux/utils';
import Dispatcher from '../dispatcher/Dispatcher';

class MessageStore extends ReduceStore {
  getInitialState() {
    return { message: 'おす、おらFlux' };
  }

  reduce(state, action) {
    switch (action.type) {
    case 'UPDATE_MESSAGE':
      return Object.assign({}, state, {
        message: action.message
      });

    default:
      return state;
    }
  }

  getMessage() {
    return this.getState().message;
  }

}
export default (new MessageStore(Dispatcher));

Dispatcher.dispatchで投げられたActionはreduce(state, action)で受け取れる。 受け取った値でstateを変更したら、必ず、Object.assign({}, state...して、新しstateを返す必要がある。

これで以下のようになる。

一番簡単なfluxアクションつき

TODOアプリをFluxにする

前回作ったTODOアプリもFluxにしてみる。

/07/add_todo.jsx

import React from "react";

export default class AddTodo extends React.Component {
  render() {
    const {
      onChange,
      onAdd,
      value
    } = this.props;

    const _onSubmit = (ev) => {
      ev.preventDefault();
      onAdd(value);
    };
    const _onChange = (ev) => {
      onChange(ev.target.value);
    };
    return (
      <form onSubmit={_onSubmit}>
        <input type="text" onChange={_onChange} value={value}/>
        <input type="submit" value="add"/>
      </form>
    );
  }
}

AddTodo.propTypes = {
  onChange: React.PropTypes.func.isRequired,
  onAdd: React.PropTypes.func.isRequired,
  value: React.PropTypes.string.isRequired
};

新規TODOの入力フィールドのコンポーネント。 前回とほぼ同じ内容だけど、event.preventDefault()とかはコンポーネント側の役割かなということでこちらで実行。 propsで渡されたコールバックには値のみを渡すようにする。

/07/todo_list.jsx

import React from "react";

export default class TodoList extends React.Component {
  render() {
    const {
      list,
      onClose
    } = this.props;

    const getOnClick = (idx) => {
      const _onClick = (ev) => {
        ev.preventDefault();
        onClose(idx);
      };
      return _onClick;
    }
    return (
      <ul>
        { list.map((d, idx) => {
          return <li key={idx}><input type="checkbox" onClick={getOnClick(idx)}/>{d}</li>
        }) }
      </ul>
    );
  }
}

TodoList.propTypes = {
  list: React.PropTypes.arrayOf(React.PropTypes.string),
  onClose: React.PropTypes.func.isRequired
};

TODOの一覧リスト。 こちらも同様にevent.preventDefault()の部分を調整。

/07/app.jsx

import React from "react";
import { Container } from 'flux/utils';
import AddTodo from "./add_todo.jsx";
import TodoList from "./todo_list.jsx";
import TodoStore from "./stores/TodoStore";
import ActionCreator from "./action/ActionCreator";

class App extends React.Component{
  render (){
    const {
      todo,
      todoList
    } = this.state;
    return (
      <div>
        <h1>Flux TODO</h1>
        <AddTodo onChange={ActionCreator.updateTodo} onAdd={ActionCreator.addTodo} value={todo}/>
        <TodoList list={todoList} onClose={ActionCreator.closeTodo}/>
      </div>
    );
  }
}

App.getStores = () => {
  return [ TodoStore ];
};

App.calculateState = (_prevState) => {
  return {
    todo: TodoStore.getTodo(),
    todoList: TodoStore.getTodoList()
  };
};

const app = Container.create(App);

export default app;

データ操作系のpropsにはActionCreatorのメソッドを渡す。 storeは、新設するTodoStoreのみを使用し、新規TODOの入力フィールドに相当するtodoと、TODO一覧に相当するtodoListのゲッターを定義する。

/07/action/ActionCreator.js

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

class ActionCreator {}

ActionCreator.updateTodo = (todo) => {
  Dispatcher.dispatch({
    type:  'UPDATE_TODO',
    todo: todo
  });
};

ActionCreator.addTodo = (todo) => {
  Dispatcher.dispatch({
    type:  'ADD_TODO',
    todo: todo
  });
};

ActionCreator.closeTodo = (index) => {
  Dispatcher.dispatch({
    type:  'CLOSE_TODO',
    index: index
  });
};

export default ActionCreator;

各アクションを定義して、Dispatcherにアクションを投げる。

/07/stores/TodoStore.js

import { ReduceStore } from 'flux/utils';
import Dispatcher from '../dispatcher/Dispatcher';

class TodoStore extends ReduceStore {
  getInitialState() {
    return {
      todo: 'おす、おらFlux',
      todoList: ['寝る','起きる','休む','食べる']
    };
  }

  reduce(state, action) {
    switch (action.type) {
    case 'UPDATE_TODO':
      return Object.assign({}, state, {
        todo: action.todo
      });

    case 'ADD_TODO':
      if( !action.todo ) return state;

      const list = state.todoList.slice();
      list.push(action.todo);
      return Object.assign({}, state, {
        todo: '',
        todoList: list
      });

    case 'CLOSE_TODO':
      state.todoList = state.todoList.filter((item, index)=>{
        return index !== action.index;
      });
      return Object.assign({}, state);

    default:
      return state;
    }
  }

  getTodo() {
    return this.getState().todo;
  }

  getTodoList() {
    return this.getState().todoList;
  }

}
export default (new TodoStore(Dispatcher));

reduce(state, action)に各アクションに相当する処理を定義する。

これでこうなる。

TODOでflux

感想

ライブラリ使ってる割にはめんどくさいw、命名規約ありでもいいから記述をもっと楽させてほしい。