これまで作ったtodoリストにFluxを適用するのが目標だけど、その前にゼロからFluxを使ったサンプルを作って入門しておこう。
というわけで任意のディレクトリでtodolistとは別のプロジェクトを作ろう。
create-react-app hello-flux作成されたディレクトリに移動して、
cd hello-fluxFluxを追加しよう。
yarn add fluxあ、要らないものは消しておこう。
del /q src\*数字が表示されていて、ボタンをぽちぽち押すとカウントアップするシンプルな例で進めよう。
まずはビューを作ろう。
次のコードをsrc/index.jsに書いてyarn startで起動しよう。
(小さいコード&説明がしやすいのでものぐさ発揮してsrc/index.jsに書いちゃっているけれど、これをsrc/components.jsとかsrc/component-structure.jsに分割するのも自習ネタにいいかも)
import React from 'react';
import ReactDOM from 'react-dom';
const Counter = ({ count }) => (<h1>{count}</h1>);
const IncButon = () => (
<div>
<button>+1</button>
</div>
);
const App = () => (
<div>
<Counter count={0} />
<IncButon />
</div>
);
ReactDOM.render(<App />, document.getElementById('root'));このあとは次の手順で進める。
- アクションのタイプを定義する
- ボタンに
onClickを追加してアクションを呼ぶ - アクションを受け取る
Storeを作る Storeをビューに組み込む
Fluxではアクションのタイプを定数で定義する。
今回は「インクリメントする」というアクションだけ。 (自習のネタとして「デクリメントする」とか「0にリセットする」みたいなタイプを追加するのもいいかも)
src/action-types.jsというファイルを作って次の内容を書こう。
export default {
INCREMENT: Symbol()
};Symbolはユニークでイミュータブルな値を作れる型で、こういったキー項目を表すのに最適だ。
次はボタンからアクションを呼ぶよにしてみよう。
アクションの中身はFluxのDispatcherを呼び出すようにする。
Dispatcherというのは何かというと……説明が難しいな。
ええと、アクションを受け取って内部に保持しているStoreに対してディスパッチするものなんだけど、そんな言葉だけで理解出来たら苦労はしないよね。
まあ、とりあえず書きながら理解していこう。
Dispatcherはインスタンス化する必要がある。
src/dispatcher.jsに次のコードを書こう。
import { Dispatcher } from 'flux';
export default new Dispatcher();このDispatcherインスタンスを使ってアクションを渡す(?)起こす(?)発火する(?)わけだ。
(説明の言葉が難しい……あとで言い回しを修正するかも)
src/actions.jsに次のコードを書こう。
import CounterDispatcher from './dispatcher';
import ActionTypes from './action-types';
export const increment = event => {
const value = 1;
CounterDispatcher.dispatch({
type: ActionTypes.INCREMENT,
payload: { value }
});
};何をしているかというと、すごく単純で、CounterDispatcherと名付けたDispatcherインスタンスのdispatchメソッドを呼び出している。
引数はtypeとpayloadを持っていて、typeにはアクションのタイプを設定している。
payloadはさらにvalueというプロパティを持っているが、今回は足しこむ数値をこのようにvalueとして渡すようにした。
(payloadにはアクションの付加情報として自由に値を渡せて、何を渡すかはアプリケーションの設計次第だ)
ボタンから、このアクションを呼び出すようにしておこう。
src/index.jsに次のimportを追加して、
import { increment } from './actions';IncButtonにonClickを足そう。
const IncButon = () => (
<div>
<button onClick={increment}>+1</button>
</div>
);次はアクションを受け取るStoreを作ろう。
Storeはデータ(Fluxの文脈だと「状態」と呼ばれることが多いと思う)の入れ物だ。
src/store.jsに次のコードを書こう。
import { ReduceStore } from 'flux/utils';
import CounterDispatcher from './dispatcher';
import ActionTypes from './action-types';
class CountStore extends ReduceStore {
getInitialState() {
return 0;
}
reduce(state, { type, payload }) {
switch (type) {
case ActionTypes.INCREMENT: {
const { value } = payload;
return state + value;
}
default:
return state;
}
}
}
export const countStore = new CountStore(CounterDispatcher);少しずつ見ていこう。
まずクラスとメソッドの宣言に注目しよう。
class CountStore extends ReduceStore {
getInitialState() {
}
reduce(state, { type, payload }) {
}
}CountStoreクラスはReduceStoreクラスを継承している。
ReduceStoreはFluxが用意してくれている基底クラスだ。
メソッドはgetInitialStateとreduceの2つが定義されている。
これらはReduceStoreからオーバーライドしたものだ。
getInitialStateはStoreの初期状態を返すメソッドだ。
今回は単に0を返している。
つまりカウンターの初期値は0だということ。
getInitialState() {
return 0;
}reduceはアクションを受け取ると呼び出されるメソッドで、現在の状態(state)とアクション({ type, payload })を引数に取って、新しい状態を返すメソッドだ。
現在の状態から新しい状態を導き出す処理は、アクションのタイプによって異なる。
アクションのタイプによる処理の振り分けにはswitchを使うのが良い。
今回はINCREMENTアクションの時にpayloadから取り出したvalueを現在の状態に足して新しい状態を導き出している。
なお、未知のアクションを受け取った場合は現在の状態をそのまま返すのが普通だ。
reduce(state, { type, payload }) {
switch (type) {
case ActionTypes.INCREMENT: {
const { value } = payload;
return state + value;
}
default:
return state;
}
}これでStoreが出来た。
最後にStoreをビューに組み込もう。
この手順を終えれば、Storeに変更があればビューに通知されて勝手にrenderしてくれるようになる。
src/index.jsに次のimportを足して、
import { Container } from 'flux/utils';
import { countStore } from './store';AppでCounterに0を固定で渡していたところをcountを渡すように変えて、
const App = ({ count }) => (
<div>
<Counter count={count} />
<IncButon />
</div>
);AppがStoreの変更を受け取るようにして、
const getStores = () => [countStore];
const calculateState = () => {
return { count: countStore.getState() };
};
const AppContainer = Container.createFunctional(App, getStores, calculateState)Appの代わりにAppContainerをrenderするように変更する。
ReactDOM.render(<AppContainer />, document.getElementById('root'));色々なことを一気にやりすぎた。 少しずつ見ていこう。
肝になるのはAppがStoreの変更を受け取るようにしたところだ。
Container.createFunctionalにビュー(App)と、状態が変更されたときにビューに通知をする対象となるStoreを返す関数(getStores)と、ビューが使用するStoreを返す関数(calculateState)を渡して新たなビューを作成している(AppContainer)。
getStoresはStoreの配列を返すだけのアロー関数。
ここで返されたStoreの状態が変更されたとき、ビューに通知されてビューはrenderを呼び出すようになる。
つまり、この関数が自動でrenderを呼び出す仕組みの一旦を担っている。
calculateStateはビューが使用する状態を返すアロー関数。
今回はcountという名前でcountStoreが持つ状態を参照できるようにしている。
ここで設定された状態はContainer.createFunctionalに渡されるビュー(ここではApp)のprops(つまりアロー関数の引数)として渡される。
最後にsrc/index.jsの全体を掲載しておく。
import React from 'react';
import ReactDOM from 'react-dom';
import { increment } from './actions';
import { Container } from 'flux/utils';
import { countStore } from './store';
const Counter = ({ count }) => (<h1>{count}</h1>);
const IncButon = () => (
<div>
<button onClick={increment}>+1</button>
</div>
);
const App = ({ count }) => (
<div>
<Counter count={count} />
<IncButon />
</div>
);
const getStores = () => [countStore];
const calculateState = () => {
return { count: countStore.getState() };
};
const AppContainer = Container.createFunctional(App, getStores, calculateState)
ReactDOM.render(<AppContainer />, document.getElementById('root'));以上の手順をこなせばボタンを押すとカウントアップするものが出来たと思う。
ここでいくつか課題を出そう。
- 押すと値をカウントダウン(デクリメント)するボタンを追加してみよう
- 押すと値を
0にリセットするボタンを追加してみよう - どのタイプのアクションが呼び出されたのかを持つ
Storeを追加して、画面に履歴表示してみよう todolistにFluxを組み込んでみよう
なお、todolistのサンプルコードにはFluxを組み込んだものが既にあるので参考にしてみてね。