こんにちは。エンジニアの安達(@ry0_adachi)です。
今回はReact + FluxをFlux Utilsを使って導入するための話をしたいと思います。
この記事を書こうと思った理由
普段ちょっとしたツールなんかをReactを使って実装したりするのですが、その時にReduxとかでやっているとファイル増えたりしてすごく冗長だなあと感じて、もっと薄く実装できるライブラリを使おうと思った時に手に取ったのがFlux Utilsでした。
この記事ではFlux UtilsにおけるFluxアプリケーションの実装方法に加えて、そもそもReactやFluxって今までのライブラリやフレームワークと比べて何がいいのか?について説明していきます。初歩的な書き方やjsxだったりについては話さないのでドキュメントなどを読んでいただければと思います。
ドキュメント
ReactとFluxを導入するにあたって
まず、React + Fluxの構成で今まで他のライブラリやフレームワークを使っていて感じたフロントエンドにおける課題がそもそも解決できるのかを先に考えてみます。
Reactとコンポーネント指向
ReactはFacebookが開発しているViewライブラリで、UI(画面の要素: ヘッダ, メニュー etc...)ごとにViewを分割し、複数のViewによって1つの画面を構成します。これによってUIの管理や実装が小さい単位で行えるので複雑になりにくいです。
jQueryなどで実装していると画面内のUIが大量に詰め込まれたファイルが出来上がることがありましたが、ReactではUIごとに分割するアプローチをとることでUIの実装が膨らんできても各ファイルの中身はシンプルでコンポーネントの管理が非常に楽です。
コンポーネント間での状態管理
ReactによってUIの管理は楽になりますが、アプリケーションで扱う状態の管理をどうするか?という問題があります。ReactはViewの開発をよりよくするためのアプローチなので別の手段が必要です。UIに関係しないコード(APIとの通信, データの登録, 更新, 削除)が紛れてしまえばシンプルに実装されたViewも結局は複雑で管理しにくいコードになってしまいます。
そこで、上記の問題を解決するためにFluxを利用します。
Fluxの状態管理と責務
FluxはFacebookによって考案されたアーキテクチャです。Fluxの考え方に「状態の流れは常に一方向」というものがあり、これは「UI操作に伴う複雑な状態の遷移を分かりやすくするもの」だと私は思っています。
状態の流れを意識しないで実装していると、どこで状態が変化したかが追いづらいコードになります。ユーザの入力によって複数のイベントを発火させたりすることもあり、それを管理し把握できていないことで複雑な実装になりがちな上に意図していない挙動を引き起こすことも多いです。
Fluxではデータに関する責務を下記のように分割します。
- Action (Action Creator)
- Dispatcher
- Store
- View
これによって状態管理をReactが担当するViewの外に出し、Viewは外部からデータを受け取りUIを表示する、といったシンプルな実装を実現することができます。
また、状態が流れる方向は上記で記述した順番で固定されており、Fluxのルール通りに実装していれば逆流させることはありません。
Flux Utilsで実装する
Fluxと同じリポジトリに入っているFluxを実装するための最低限の機能を備えたライブラリです。Flux Utilsがカバーする範囲はStoreとViewのみですが、これまた同じリポジトリにDispatcherの実装が入っているのでこれも使います。
今回は入力した文字列をボタンをクリックしたら保存して入力された文字列全てを表示するというアプリを作ってみます。
Action (Action Creator)
Actionがやることは主に下記の3つになると思います。
- 外部(API)との通信
- データの加工
- Dispatcherにtypeと値を渡す
Viewからの要求を処理してその結果をStoreに渡すのがActionの役割です。外部との通信は全てここで行い、Store内で更新のためのロジックが複雑にならないようにデータの加工とかが必要なのであればActionで加工してしまった方がStoreがスッキリするので良いと思います。データをStoreに渡す準備ができたらDispatcherに対してdispatchします。
import { dispatch } from '../dispatcher/Dispatcher' const Actions = { changeText: (text) => { dispatch({ type: 'change_text', text: text }) } } export default Actions
dispatchする時に入力値などの他にtypeを必ず指定します。Dispatcherの先にいるStoreはtypeを見てどのコールバックを実行するかを決めるのでtypeがなければ適切に処理することができなくなります。
Dispatcher
DispatcherはActionsからdispatchされ、自身に登録されているコールバックに情報を渡す、という役割があります。また、複数のコールバックを実行する際にはその順番の制御を行うためのインタフェースも提供します。
import { Dispatcher } from 'flux' const instance = new Dispatcher() export default instance export const dispatch = instance.dispatch.bind(instance);
今はDispatcherの基本実装だけで事足りるのでインスタンスをそのまま作って準備OKです。ただ、Actionから呼び出す時に毎回Dispatcher.dispatch()
という書き方だと長いのでdispatchだけで呼べるように別途exportしておきます。
Store
Storeは状態(State)を管理するために下記のような役割を持っています。
- Viewがアクセスするためのgetterの提供
- Dispatcherに対するコールバックの登録
- Stateの管理と生成
StoreはStateの更新を行います。更新はDispatcherに登録したコールバックからのみ実行します。そうすることで状態の流れる方向が保たれます。
今回はFlux Utilsの提供する3つのStoreの中の1つであるReduceStoreを使ったサンプルを実装します。
import { ReduceStore } from 'flux/utils' import Dispatcher from '../dispatcher/Dispatcher' class TextStore extends ReduceStore { getInitialState() { return [] } reduce(state, action) { switch (action.type) { case 'change_text': state.push({ text: action.text }) return state.slice(0) default: return state } } } const instance = new TextStore(Dispatcher) export default instance
Flux Utilsではもともと保持していたStateと比較して別オブジェクトであれば再描画を行う、という仕組みのため渡ってきたstateをそのまま使うと差分がないとみなされて再描画が実行されません。なので今回はArray.sliceでやってますが、Immutable.jsなどを使ってStateの生成を行うのが良いと思います(毎回sliceとかconcatとか書きたくないですし...)。
View
Flux UtilsではContainerというReact Componentのラッパーがあります。ContainerとReact Componentをそれぞれ説明していきます。
Container
ContainerはStoreからStateを受け取り、そのStateが更新されていれば自身の配下にいるComponentに変更を通知し、再描画が実行されます。
import { Container } from 'flux/utils'; import React from 'react' import TextStore from '../../store/TextStore' import TextInput from '../../components/TextInput' import TextList from '../../components/TextList' class TextApp extends React.Component { static getStores() { return [TextStore] } static calculateState() { return { items: TextStore.getState() } } render() { return ( <div> <TextInput save={this.save} /> <TextList items={this.state.items} /> </div> ) } } const TextAppContainer = Container.create(TextApp) export default TextAppContainer
TextStoreの管理するStateが変更された時にTextContainerはTextInputとTextListに変更を通知し、再描画させます。React単体だとStateにsetしたりと色々と管理することが多くViewの中が汚れてしまったりすることもありますがContainerから伝搬されてくるもの、という状態を守っていれば管理は楽になるはずです。
React Component
ComponentはContainerから受け取ったStateからUIの表示を行います。また、各イベントとactionを紐づけることでFluxのサイクルで状態が更新されます。これによって外部から与えられるStateについての管理や親にコールバックを戻すといった複雑な処理が減り、ComponentはよりシンプルにUIに集中することができます。
// TextInput import React from 'react' import Actions from '../../actions/Actions' class TextInput extends React.Component { constructor(props) { super(props) this.state = { text: '' } } handleChange(e) { this.setState({ text: e.target.value }) } handleClick(e) { e.preventDefault() this.setState({ text: '' }) Actions.changeText(this.state.text) } render() { return ( <div> <input type="text" value={this.state.text} onChange={this.handleChange.bind(this)} /> <button onClick={this.handleClick.bind(this)}> Save </button> </div> ) } } export default TextInput
// TextList import React from 'react' class TextList extends React.Component { render() { const items = this.props.items.map((item, index) => { return <p key={index}>{item.text}</p> }) return ( <div>{items}</div> ) } } export default TextList
サンプルリポジトリ
コード全体を確認したり、実際に動かしたりするためにwebpack-dev-serverを組み込んだリポジトリを用意しました。こちらのコードも合わせて読んでみてください。
まとめ
Reduxなどを使って実装するとどうしても最初のコード量やファイル数が増えとっつきにくい(そもそもどの位実装したらこのコストをペイできるのか、という疑問もあり...)、という方でもFlux Utilsなら実装がしやすいと思います。また、ちょっとしたアプリケーションにも向いていると思います。
ただ、Flux Utilsは最低限のFlux実装を行うものであり大規模であったり複雑な実装を求められる場合に別の手段を用いる必要もあるんじゃないかな、という不安もあります。
Connehitoとしてもこれから使っていくぞ!というところなのでまだまだ実務での知見は多くないです。これから実装を進めていく中で壁にぶつかったり何かいい手段を見つけたらこちらでまた共有しようと思います。