Table of Contents
ReactとReduxを学ぶために、開発環境というかプロジェクトテンプレートをスクラッチから作っている。 (最終的な成果はGitHubに置いた。)
前回はMaterial-UIをセットアップした。
(2018/11/21更新)
Reactの状態管理
Reactによるプログラミングをするとき、小さいUIコンポーネントをたくさん作って、それらを組み合わせてVirtual DOMツリーを作っておいて、そこにpropsをほうりこんでレンダリングする、という感じになる。 また、レンダリングした後はコンポーネントのstateをいじって状態を変化させる。
このpropsやstateの扱いをReactの状態管理という。 propsやstateを適当にアドホックに設定してると、結局jQuery使ってるのとそんなに変わらなくなって辛くなるので、Reactの開発元であるFacebookはFluxというアーキテクチャを提案している。

Fluxでは、単一の(またはドメイン毎くらいの単位の)オブジェクトでアプリケーション全体の状態(state)を表し、これをStoreに保持する。 ReactはStoreが保持するstateを受け取り、それをもとにViewをレンダリングする。 Viewに対するユーザの操作(など)はActionというオブジェクトで表現され、Dispatcherに渡され、Dispatcherに登録されたcallbackを通してstateを変化させる。
データが常に一方向に流れて見通しがよく、各コンポーネントの独立性が高いのが特徴。 各コンポーネントは、受け取ったデータをピュアに処理すればよく、リアクティブにファンクショナルに実装できる。
Redux
Fluxの実装、というか発展形がRedux。
ReduxではFluxのDispatcher辺りがReducerに置き換わっている。 ReducerはActionと現在のstateから次のstateを計算する純粋関数。
また、ReduxからはViewが切り離されていて、Actionによってstateを更新する状態管理ライブラリの役割に徹している。 ReactコンポーネントのイベントハンドラからActionオブジェクトを生成したり、更新したstateをReactに渡したりするつなぎ目は、別途React Reduxというライブラリが担当する。
ReduxとReact Reduxについては、Qiitaの「たぶんこれが一番分かりやすいと思います React + Redux のフロー図解」という記事が分かりやすい。
今回はReduxを導入する。
$ yarn add reduxRedux v4.0.1が入った。
以降、現時点で唯一のUIコンポーネントであるHOGEボタンの状態管理を実装してみる。
Action
まずActionを実装する。
Actionオブジェクトはどんな形式でもいいけど、普通はFlux Standard Action(FSA)にする。 FSAは以下のプロパティを持つプレーンオブジェクト。
- type: Action種別を示す文字列定数。必須。
- payload: Actionの情報を示す任意の型の値。任意。
- error: Actionがエラーを表すものかを示す boolean プロパティ。エラーなら true にして、payload にエラーオブジェクトをセットする。任意。
- meta: その他の情報を入れる任意の型の値。任意。
Actionのコードは、Actionのtypeに入れる値を定義するactionTypes.jsと、Action Creator(i.e. Actionオブジェクトを生成する関数)を定義するactions.jsからなり、ともにsrc/actions/に置く。
HOGEボタンをクリックしたときのAction、HOGE_BUTTON_CLICKEDを定義してみる。
src/actions/actionTypes.js:
export const HOGE_BUTTON_CLICKED = 'HOGE_BUTTON_CLICKED';
src/actions/actions.js:
import {
HOGE_BUTTON_CLICKED,
} from './actionTypes';
export function hogeButtonClicked() {
return {
type: HOGE_BUTTON_CLICKED,
};
}
こんな感じ。
Reducer
次はReducer。
Reducerは、上記Action Creatorが生成するActionオブジェクトに対応して起動し、Store(後述)から現在のstateオブジェクトを受け取って、Actionオブジェクトのpayloadの値(など)に応じて新しいstateオブジェクトを作る。
Reducerを書く前に、stateオブジェクトの構造を設計しておくことが推奨されている。 UIコンポーネント毎にプロパティを分けて、コンポーネント構造と同様の階層構造にしておけばだいたいよさそう。
HOGEボタンに一つ、クリックしたかどうかの状態(clicked)を持たせるとすると、stateオブジェクトは以下のようになる。
{
hoge: {
clicked: false,
},
}
Reducerはピュアじゃないといけないので、内部で[副作用](https://ja.wikipedia.org/wiki/%E5%89%AF%E4%BD%9C%E7%94%A8_(%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%A0) を起こしてはいけない。 副作用とは、具体的には以下のようなもの。
- 引数で与えられたオブジェクトを変更する。
- REST APIへのリクエストを送る。
(ログの出力も厳密には副作用なんだろうけど、それは許されてる気がする。)
また、ピュアであるためには参照透過性を持たないといけなくて、つまり同じ引数に対しては同じ戻り値を返さないといけないので、内部でDate.now()とかMath.random()とかを呼ぶのもダメ。
Reducerのコードはsrc/reducers/に置く。
HOGE_BUTTON_CLICKEDが発生したら、hogeのclickedをtrueにするReducer(hoge())は以下の感じに書ける。
src/reducers/reducers.js:
import { HOGE_BUTTON_CLICKED } from '../actions/actionTypes';
const initialState = {
hoge: {
clicked: false,
},
};
export const hoge = (state = initialState, action) => {
switch (action.type) {
case HOGE_BUTTON_CLICKED:
const newHoge = {
hoge: {
clicked: true,
},
};
return Object.assign({}, state, newHoge);
default:
return state;
}
}
hoge()のポイントはたくさんある。
stateとactionを引数に取る。前者が現在の状態を表すstateオブジェクトで、後者がActionオブジェクト。- 戻り値は新しい状態を表すstateオブジェクト。
- actionオブジェクトはどのActionを表すものかは分からないので、
action.typeを見てHOGE_BUTTON_CLICKEDだけを処理するようにする。- 知らないActionだったら(i.e.
default句のなかに来たら)、受け取ったstateオブジェクトをそのまま返す。
- 知らないActionだったら(i.e.
- アプリケーションの初期化時には
stateにundefinedが渡されるので、それに備え、初期状態であるinitialStateをデフォルト引数に設定する。 - 渡されたstateオブジェクトを変更してはいけないので、Object.assgin()に空オブジェクト
{}とともにstateを渡してコピーする。Object.assgin()の代わりにオブジェクト分割代入を使う方法もある。この場合babel-plugin-transform-object-rest-spreadが必要。
Object.assign()の第三引数にnewHogeで上書きするようにしている。- 今はstateオブジェクトのプロパティが
hoge一つだけなので単にnewHogeをreturnしても結果は一緒。なので無駄なことをしてるようにも見えるけど、stateオブジェクトのプロパティが増えた場合にhoge以外に影響を与えないための計らい。
- 今はstateオブジェクトのプロパティが
これはこれでいい感じに見えるけど、hoge()がhogeプロパティしか扱わないのに、stateオブジェクト全体を渡しているのがイケていない。
(まあ今はstateオブジェクトにはhogeプロパティしかないんだけど、他のプロパティが色々増えてくるとイケてない感が高まる。)
hogeプロパティがstateオブジェクト構造のどこにあるかをhoge()が気にしないといけないのもイケてない。
hoge()にはhogeプロパティだけを見てほしい。
ということで、普通はReducerは分割して書いて、それぞれのReducerにstateオブジェクトを分割して渡してやる。
src/reducers/reducers.js:
import { HOGE_BUTTON_CLICKED } from '../actions/actionTypes';
-const initialState = {
- hoge: {
- clicked: false,
- },
-};
-export const hoge = (state = initialState, action) => {
+export const hoge = (state = { clicked: false }, action) => {
switch (action.type) {
case HOGE_BUTTON_CLICKED:
const newHoge = {
- hoge: {
- clicked: true,
- },
+ clicked: true,
};
return Object.assign({}, state, newHoge);
default:
return state;
}
}
+export const rootReducer = (state = {}, action) => {
+ return {
+ hoge: hoge(state.hoge, action),
+ }
+}
こんな感じで、rootReducerがstateオブジェクトを分割して子Reducerを呼び出す。
孫Reducerとか曾孫Reducerとかがあってもいい。
rootReducerは別のファイルに書くと見やすくなるし、ReduxのcombineReducers()というヘルパー関数を使うともっと楽に書ける。
上記reducers.jsからはrootReducerを削除して、rootReducer.jsに以下のように書く。
src/reducers/rootReducer.js:
import { combineReducers } from 'redux';
import hoge from './reducers';
const rootReducer = combineReducers({
hoge,
});
export default rootReducer;
このようにcombineReducers()で作ったrootReducerは、上で自前で書いたrootReducerと全く同じ動きをする。
さらに簡単に、以下のようにも書ける。
src/reducers/rootReducer.js:
import { combineReducers } from 'redux';
import * as reducers from './reducers';
const rootReducer = combineReducers(reducers);
export default rootReducer;
こうしておけば、Reducerの追加はreducers.jsに関数を追加するだけでよくなる。
redux-actionsを使うとさらに記述を簡略化できるみたいだけど、逆に何が何だか分からなくなりそうだったので、慣れるまでは使わないでおく。
Store
Storeは以下のような特徴を持つオブジェクト。
getState()でstateオブジェクトを返す。dispatch(action)でActionをディスパッチできる。subscribe(listener)でActionのディスパッチをサブスクライブできる。
StoreはrootReducerをcreateStore()に渡すことで作れる。
createStore()を呼ぶコードはモジュールにしておくのがいい。
後で膨らんでくるので。
src/configureStore.js:
import { createStore } from 'redux';
import rootReducer from './reducers/rootReducer';
export default function configureStore(initialState = {}) {
const store = createStore(
rootReducer,
initialState,
);
return store;
}
これだけ。
以上でReduxのコンポーネントが一通りそろって、状態管理システムができた。 試しに動かしてみる。
src/try.js:
import { hogeButtonClicked } from './actions/actions';
import configureStore from './configureStore';
const store = configureStore();
console.log(store.getState()); // => { hoge: {clicked: false} }
store.subscribe(() => {
console.log(store.getState());
});
store.dispatch(hogeButtonClicked()); // => { hoge: {clicked: true} }
store.dispatch()するとReducer(hoge())が実行され、stateオブジェクトが更新されることが分かる。
次回は、今回作ったStoreをReactコンポーネントにつなぐ。