これから始めるReact.js 発展編 第1回 Fluxという設計思想
React.jsで複雑なアプリケーションを構築するのに親和性が高い設計思想Fluxを解説します。必ず採用しなければならないものではありませんが、React.jsをよりよく理解するためにも知っておきたい設計思想です。
- カテゴリー
- UIフレームワーク >
- React/Preact
発行
本シリーズ内で紹介している内容は利用できますが、現在のバージョンでは新しい書式などが追加されているので注意してください。(2020年7月現在)
はじめに
このシリーズでは、入門シリーズの先にあるReact.jsの関連トピックとして、代表的なものを取り上げていきます。
まずは、React.jsでのアプリケーション構築におけるFlux(フラックス)について解説します。
なお、このシリーズを読み進める前に、入門のシリーズ*でReact.jsの基本的な使い方を押さえておくことをおすすめします。
*注:入門シリーズ
- 「これから始めるReact.js」シリーズ
Fluxとは
React.jsと同じく混同されがちですが、Fluxも特定のライブラリやフレームワークを指す言葉ではありません。
しいて分類するなら、MVCパターンと同じ、アプリケーションの設計思想・デザインパターンの1つです。
Fluxは、Facebookのドキュメントにも、**It complements React's composable view components by utilizing a unidirectional data flow.**とあるように、データの流れを一方向(unidirectional)に限定するのが特徴的な設計パターンです。
そしてこの思想に沿ってアプリケーションを構築していくうえで、定型化した処理をまとめたライブラリも、すでにたくさんあります。
代表的なものを次に挙げておきます。
それぞれ、似ていながらも異なる特徴があり、今のところ唯一の選択肢があるという状況ではないと筆者は考えています。プロジェクトの規模や要件に応じて、適切なものを選択していくことになります。
もしくは、このパターンに準じたコードを自分で書いていくことになります。
また、どのライブラリを採用しても基本的な考え方は変わりません。よってこのシリーズでは、各ライブラリの実装にフォーカスしていくのではなく、まずはFluxという思想自体の基本的な部分にフォーカスして解説していきます。
この記事ではfacebook/fluxを参考実装として取り上げますが、これはライブラリの実装にフォーカスする目的ではなく、あくまでも解説の便宜上、一例として取り上げていると理解してください。
後述しますが、このライブラリは、Fluxの核となる機能であるDispatcher
と、より踏み込んだFlux Utils
という機能群を提供します。
Fluxが有効となるケース
まず、前提ですが、「React.jsを使う = 必ずFluxを採用するべき」というものでは決してありません。
Fluxの採用が有効なのは、複雑にコンポーネントがネストしており、それに伴ってデータフローも複雑化している状況です。
よって、数多くのコンポーネントで構成されていようとも、データの更新が単純であったり、構造化されたデータをただ表示するだけのアプリケーションであれば、Fluxを取り入れる必要性*はないとも言えます。もしくは、より簡潔な実装パターンを採用することでも解決できるでしょう。
*注:Fluxを取り入れる必要性
筆者としては、React.jsである程度コードを書いていくと、最適化の末に自然とFlux的な考え方・コードにたどり着くことも多いのではと思っています。もしあなたが今「Fluxが必要かどうかわからない」段階なのであれば、おそらくまだ必要ないのだとも思います。
また、Fluxはあくまで考え方であるため、React.jsだけでなく、Vue.jsやAngularJSを使っていても採用することが可能です。
facebook/fluxの構成要素
以下の図は、先ほどあげたfacebook/fluxのドキュメントでもよく見られるデータの流れを表しています。 Fluxの特徴であるデータフローが一方向である様子がわかります。
このデータフローを実現するための機能をまとめたものを、Fluxの実装と呼びます。
Fluxにおける機能の概要
これから順にそれぞれの機能の役割について解説していくのですが、まずは前掲の図が表す意味と、各機能の役割を箇条書きにしておきます。
- Action
- Viewで起きたユーザーのイベントや、サーバーへのデータのリクエストなど、アプリケーションの状態への働きかけを表す単位
- Dispatcher
- Actionを受け取り、Storeから紐づけられたコールバックを実行する
- Store
- Dispatcherに紐付けたコールバックにより、管理しているデータを更新する
- View
- Storeのデータ更新に追従し、そのデータをそのまま表示する。React.jsを採用した場合は、Reactコンポーネント
- ユーザーが商品をカートに入れた = カートの一覧の状態を更新する
- サーバーから商品リストを取得した = 商品リストの状態を更新する
- Storeから登録されたコールバックを保持する
- Actionを受け取り、登録されたコールバックを呼び出す
- Actionが発行され
- Dispatcherがそれを捌き
- Storeのデータが更新され
- Viewのデータも合わせて更新される
- Viewが自身にDispatcherを
dispatch()
するメソッドを継承しているもの - Dispatcherはコールバックを保持するのではなく
EventEmitter
がハブとなるもの - Storeのデータ更新処理を別の概念に切り出したりするもの
データの管理はStoreという機能に任せ、Actionのやり取りによってのみデータを更新し、その結果Viewが更新される。そして、これらを中心でとり持つのがDispatcherという構成です。ひらたく言ってしまえばObserverパターン*(Pub/Subパターンとも)の再定義とも言えます。
*注:Observerパターン
オブザーバーは状態の変化をイベントを発行することで通知します。この通知によって、イベントを購読している側で状態の変化を検知することができるというパターンです。
Fluxを導入したからといって、コンポーネントの作り方が大きく変わるわけではありません。Fluxは、あくまでデータフローとデータの更新ロジックの所在を明確にするためのものです。
以降、各機能の役割と、この流れを意識して読み進めてください。
Action
Actionとは、その名のとおり、アプリケーションの状態に対する働きかけのことです。言葉ではわかりにくいですが、具体的には、次のようなまとまりです。
そしてこれらは概して以下のようなオブジェクトの形式で表されます。
Actionの実装例
{
type: 'ADD_ITEM',
item: {
name: '商品1', price: 500
}
}
どんなActionかを定義するtype
と、それに紐づくデータのひとかたまりです。
ユーザー入力による働きかけなどをすべてこの単位で正規化し、あらゆるイベントでActionを発行するようにします。
そうすることで、「アプリケーションの状態を変更するためには必ずActionを発行する」というフローが生まれます。
ただし、前掲したコードを見ればわかるように、Actionはただのオブジェクトです。実際にアプリケーションの状態を変更するために、このActionを次項で解説するDispatcherに渡す必要があります。
Actionはデータフローにおける登場人物というよりは、データフローに乗って流れる部品の1つととらえた方がわかりやすいかもしれません。
Dispatcher
Dispatcherはfacebook/fluxを用いたアプリケーションにおいて核となる存在です。後述するStoreとViewを、先ほど紹介したActionを通じて結び付けるのがその役割です。
その機能を端的に表すと、次のようになります。
他の機能とは違い、ひとつのアプリケーションに対してDispatcherはひとつです。ゆえに実装としてはシングルトンパターン*を使われることが多いです。
*注:シングルトンパターン
インスタンスが常に1つしか生成されないことを保証するような実装パターンのこと。ひとつのアプリケーションに対して、Dispatcherは常にひとつであるので、実装もシングルトンパターンを採用することになります。
先に少しだけコードで利用例のアウトラインを見ておくと、次のようになります。
Dispatcherの登録例
// Storeからのコールバックの登録
dispatcher.register((action) => {
if (action.type === 'ADD_ITEM') {
// Storeのデータを更新
// action.item は { name: '商品1', price: 500 }
}
});
Storeのデータを更新する関数をコールバックとし、Dispatcherのregister()
に渡します。
Dispatcherの実行例
// Actionを引数に、登録されたコールバックを呼び出す
// 前述のコールバックが実行される
dispatcher.dispatch({
type: 'ADD_ITEM',
item: {
name: '商品1', price: 500
}
});
受け取ったActionをdispatch()
することで、Storeから登録されたコールバックを呼び出します。コールバックは次に説明するStoreが登録するものです。
このfacebook/fluxのDispatcherには、他にも機能があるのですが、今回は説明を割愛します。
Store
Storeの役割は、アプリケーションの状態を表すデータを管理することです。データを保持することと、そのデータに対する更新を行うのもStoreの役割です。
facebook/fluxにおけるStoreは1つのアプリケーションに複数持つこともありますが、Dispatcherと同じくシングルトンパターンが採用されます。
前述のとおり、Dispatcherにコールバックを登録しておき、特定のActionが発行された場合に自身のデータを更新します。コードで表すと次のようになります。
Storeの実装例
const data = {
items: []
};
CartStore.dispatchToken = dispatcher.register((action) => {
if (action.type === 'ADD_ITEM') {
data.items.push(action.item);
}
});
ADD_ITEM
というtype
を持つActionがdispatch()
された時に実行されるコールバックです。自身のスコープにあるitems
配列に、商品情報を追加しています。
このように、StoreのデータはDispatcherに登録したコールバック経由でのみ更新されるところがポイントです。Actionが発行されない限りデータを更新することができないようにするため、StoreにはGetter
(データの取得)のみを実装し、Setter
(データのセット・更新)は実装しません。
また、Storeのもうひとつの側面として、Viewに使うデータを供給する役割があります。
ViewはStoreのデータの更新を監視して表示を行います。そのため、StoreはEventEmitter
のようなデータの更新を外部に知らせる仕組みが備わっていることが多いです。
StoreがViewにデータを供給する実装例
// EventEmitterを継承
class CartStore extends EventEmitter {
getItems() { return data.items.slice(); }
}
const cartStore = new CartStore();
cartStore.dispatchToken = dispatcher.register((action) => {
if (action.type === 'ADD_ITEM') {
data.items.push(action.item);
this.emit('CHANGE'); // このイベントを、Viewが監視
}
if (action.type === 'REMOVE_ITEM') {
data.items.splice(action.idx, 1);
this.emit('CHANGE'); // このイベントを、Viewが監視
}
});
Storeのデータ更新をViewで検知するため、そのつどイベントをemit()
しています。
View
最後にViewです。
先ほども少し紹介しましたが、Viewの持つ役割はStoreのデータを監視し、そのデータを表示することです。
Viewの実装例
class AppComponent extends React.Component {
componentDidMount() {
// Storeの更新を監視
cartStrore.on('CHANGE', this._update.bind(this));
}
_update() {
// StoreのデータでViewをすべて更新
this.setState({ cartItems: cartStore.getItems() });
}
}
このように、Storeのデータの状態を常に同期しておきます。更新されたデータは末端のコンポーネントへprops
で渡していきます。ここはFluxを採用していないパターンとも変わりありません。
またViewは、ユーザーの入力に応じてActionを発行することもあります。
ViewがActionを発行する実装例
const ItemComponent = ({
item,
addItem,
}) => {
return (
<div>
<h3>{item.name}</h3>
<button onClick={ () => { addItem(item); } }></button>
<div>
);
};
ここで呼んでいるaddItem()
は次のようになっています。
addItem()
function addItem(item) {
dispatcher.dispatch({
type: 'ADD_ITEM',
item: item,
});
}
onClick()
のユーザー入力により、自身が表示している商品の情報をActionとしてDispatcherへと渡し、Storeを更新する流れです。
まとめ
今回はFluxという設計を実際のコード例を見ながら解説しました。
という一方向のデータフローを実現する仕組みがわかったでしょうか。
こうすることで、Actionのやり取りだけに意識を集中することができるのが、Fluxを採用した場合のメリットです。データの更新処理をReact.jsのコンポーネントの外に出すこともできています。
とはいえ、達成したかったこと自体は今までのパターンとなんら変わらず、表示(View)とデータ(Store)を疎結合にすることです。
繰り返しになりますが、Fluxは新しく生まれた考え方ではありません。いわゆるObserverパターン(Pub/Subパターン)を、React.jsのようなコンポーネント指向のライブラリと組合せやすいようにアレンジしたものです。
冒頭でさまざまなライブラリが存在すると言いましたが、この基本の考え方は変わりません。ただ実装面では異なっており、
など、細かな部分でそれぞれのライブラリの特色が出てきますので、注意は必要です。
次回は、引き続きfacebook/fluxを使って実際にコードを書きながら、サンプルサイトにFluxを実装していきます。