fluxフレームワーク、今は Redux が一番アツい様子なので触ってみた。
(追記 2015-10-03 18:15 APIや用語が大幅に変わったので更新した)
Redux の経緯
2015-05-30 に公開された "The Evolution of Flux Framework" という記事がある。
Reduxはその PoC 的な実装だったんだけど、実用的ということでめっちゃ流行っているみたい。
Initial commit も 2015-05-30 だった。
Initial commit · rackt/redux@8bc1465 · GitHub
最近の flux フレームワークだと fluxible と flummox が人気だったけど、
flummoxのページには 4.0 will likely be the last major release. Use Redux instead. It's really great.
なんて書いてある
reduxの特徴
以下の特徴をサッと紹介する。
- 全ての state を
redux
オブジェクトが管理する - Store の更新ロジックや ActionCreator がステートレスな関数
- hot reload や undo, redo が簡単に実装できる
僕も理解が怪しいので、詳しくは README.md か The Evolution of Flux Framework を読んでくれ。
全ての state を store
オブジェクトが管理する
Reduxでは、全ての状態を store
オブジェクトに持たせ、コンポーネントでは store
を呼び出して必要な情報を受け取る。
↓のコードでは、まず全ての Store を登録した store
を作成し、CounterApp以下のコンポーネントから利用できるようにしている。
import { createStore } from 'redux'; import { Provider } from 'react-redux'; import * as stores from '../stores/index'; const store = createStore(stores); export default class App { render() { return ( <Provider store={store}> {() => <CounterApp /> } </Provider> ); } }
コンポーネント側では↓のようにして、state 内の必要な情報を受け取る。
import React from 'react'; import { connect } from 'react-redux'; import Counter from '../components/Counter'; @connect(state => ({ counter: state.counter })) export default class CounterApp { render() { const { counter } = this.props; return ( <Counter counter={counter} /> ); } }
この時、CounterApp 下の全てのコンポーネントで @connect
を使って情報を取り出せる。
@connect
で指定したデータは props に渡される。
Reducer が状態を持たない
Flux では、興味領域毎に Store が存在し、 Store が保持する状態の合成で世界を表現していた。
この場合、 Store 内の情報の更新ロジックは Store 自身が持っていた。
これに対し、Reduxでは、状態は全て store
の state
に保存する。
そして、state
を更新するロジックを興味領域毎に分割する。
これを Reducer と呼ぶ。
Reducer は状態を持たず、「state を受け取って次の state を返す関数」である。
結果、 state を更新するロジックが常にステートレスに保たれる。
import { ADD_TODO, DELETE_TODO } from '../constants/ActionTypes'; const initialState = [{ text: 'Use Redux', marked: false, id: 0 }]; export default function todos(state = initialState, action) { switch (action.type) { case ADD_TODO: return [{ id: (state.length === 0) ? 0 : state[0].id + 1, marked: false, text: action.text }, ...state]; case DELETE_TODO: return state.filter(todo => todo.id !== action.id ); default: return state; } }
waitFor がない
Reduxには waitFor
がないが、Reducer は単純な関数なので、Reducer の処理を合成する Reducer を作り、その中で順番を制御するとよい。
import fooReducer from './fooReducer'; import barReducer from './barReducer'; export default function bazReducer (state, action) { const foo = fooReducer(state, action); const bar = barReducer(state, action); return { foo, bar }; }
ActionCreator もただの関数
facebook/flux だと、↓のようにActionを生成して Dispatcher.dispatch
を呼び出す必要があった。
他の Flux 実装でも大体同じ。
var AppDispatcher = require('../dispatcher/AppDispatcher'); var TodoConstants = require('../constants/TodoConstants'); var TodoActions = { create: function(text) { AppDispatcher.dispatch({ actionType: TodoConstants.TODO_CREATE, text: text }); }, destroy: function(id) { AppDispatcher.dispatch({ actionType: TodoConstants.TODO_DESTROY, id: id }); }, }
Redux では ActionCreator はただの関数として書く。
import * as types from '../constants/ActionTypes'; export function addTodo(text) { return { type: types.ADD_TODO, text }; } export function deleteTodo(id) { return { type: types.DELETE_TODO, id }; }
ただ、当然 Dispatcher に登録する必要がある。 登録は Component 側で↓のようにして行う。
// CounterApp.js import React from 'react'; import { bindActionCreators } from 'redux'; import { connect } from 'react-redux'; import Counter from '../components/Counter'; import * as CounterActions from '../actions/CounterActions'; @connect(state => ({ counter: state.counter })) export default class CounterApp { render() { const { counter, dispatch } = this.props; return ( <Counter counter={counter} {...bindActionCreators(CounterActions, dispatch)} /> // dispatcher に bind ); } } // Counter.js import React from 'react'; export default class CounterApp { render() { return ( <div onClick={::this.onClick}> Count : {counter} </div> ); } onClick () { const { counter, setCounter } = this.props; setCounter(counter + 1); } }
所感
state を設計するときの考え方が変わる
これまでの Flux では、アプリケーションの動作に必要な情報をドメイン毎に分け、各ドメインの Store がそれぞれ状態を管理していた。
Redux では、ある時点でのアプリケーションの状態をすべて state に保持する(single source of truth
等と呼ばれる)。
これによって、Reducer, ActionCreator, View を書くときに状態を考える必要がなくなり、かつ hot reload 等もしやすくなる。
さらに、@connect
を通じ、どのコンポーネントからでも情報を取り出せる。
(@connect
便利すぎる。なんか Angular の DI 思い出した)
結果、コンポーネントの state を使おうという機運がダダ下がりし、state 地獄を避けられそう。
「View ではないコンポーネント」を多用する
Reducer がただの関数であり、Plain object でないエンティティを必要とするロジックの扱いが難しくなった。
ビジネスロジックを React コンポーネントで処理したくなる事がある。
react-routerみたいな。
個人的にはこういう、「View じゃないのにコンポーネント」っていうの苦手だったんだけど、SPAではこういう書き方が自然なんだな、と感じるようになった。
Modelからイベントを発行しまくれない
Reducer がただの関数になったので、 Store に当たる部分から好き勝手イベントを発行できなくなった。 そのため、定期的にイベントを発行したい場合などは View や AcitionCreator で行う。
僕が今書いてるアプリは
- View に出てこない Model 側で定期的にイベントを処理する
- それに追随して View も表示を変える
という要件がある。 当初は他の Flux フレームワークを使い、Store から定期的にイベントを発行するというのを考えていたが、Redux では上手く書けず悩んでいた。
試行錯誤の結果、「View ではないコンポーネント」を使うのが良さそうだった。 イベントを発行する条件を Reducer で処理し、コンポーネント側で定期的に Action を発行する。
// stores/playerReducer.js export default function playerReducer (state, action) { switch (action.type) { case START : return { ...state, isPlaying : true }; case STOP : return { ...state, isPlaying : false }; case TICK : return { ...state, tick : state.tick + 1 }; default: return state; } } // actions/playerActions.js export function tick () { return { type : TICK }; } // components/Player.js @connect(state => state.player) class Player extends Component { render () { return <App actions={this.props.actions} />; } spin () { this.props.actions.tick(); if (this.props.isPlaying) { setTimeout(::this.spin, 1000); // 非同期に Action を発行 } } }
Player の state で値を管理して App に渡すことでも実現できるが、子コンポーネントにいちいち props を渡す手間を考えると、 Reducer で処理するほうがラクに思える。
ES6, ES7 ガンガン使うことになり楽しい
examples や awesome-redux みると、みんな decorator とか static property とか分割代入とか ::hoge
で bindとか、ガンガン使ってる。
楽しい!!!!!!!!!!!!!!!!!!!!!!!!!