前回書いたReact.jsのコードをfacebook/fluxで書きなおしてみます。前回同様、ほぼ殴り書きです。
Fluxの考え方
基本、React.jsの担当するViewコンポーネントでは状態の変化するデータや、データを変更する処理を持たせたくない。 Fluxを適用してない前回のコードでは仕方なく、ルートコンポーネントでそれらをしてたが、Fluxを適用することで、それらをViewコンポーネントの外側でやるようにする。
データの管理やその操作は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の定義。今回はアクションがないので初期値とゲッターのみを定義。
その他のコードは前回同様。これで以下のように表示される。
アクションを定義する
ボタン押したらメッセージを変更するというアクションを追加してみる。
/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を返す必要がある。
これで以下のようになる。
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)に各アクションに相当する処理を定義する。
これでこうなる。
感想
ライブラリ使ってる割にはめんどくさいw、命名規約ありでもいいから記述をもっと楽させてほしい。