初めまして!エンジニアの米山と申します。 転職会議ではフロントエンド開発にReact.jsとReduxを利用しています。 今回はReact, Redux開発におけるGlobal stateとLocal stateという考え方について、軽く紹介させていただきます。
Redux開発の難点
ReduxはSingle source of truthという原則を採用しており、アプリケーションの状態は1つのオブジェクトに格納されます。それゆえ、アプリケーションの状態が散らばることなく管理が楽になります。
ただし、その弊害としてstateが肥大化します。stateが肥大化すると、reducerが肥大化する可能性が高まります。
対応策としては、reducerを分割したり、Normalizrのような便利なツールを使う方法が考えられます。
しかし、React自身が提供するState管理を併用することで、Reduxの責務範囲をより適切化できる可能性があります。そして結果的にstateの肥大化を多少は抑制できるかもしれません。
それがGlobal stateとLocal stateの共存です。
Global stateとLocal state
今回の記事は以下のディスカッションを参考にさせていただきました。
Redux and global state vs. local state
Global state, Local stateという考え方は上記のディスカッションで使われているものであり、この記事はその紹介、という形になります。その上で事例を作成してみて、所感を載せています。
気になる方はぜひ元記事をご覧ください。
Global stateとは
Global stateは単にReduxの管理下に置かれるstateです。
どこからでも参照できるという意味でのGlobalではなく、アプリケーションレベルで管理される程度の意味合いですね。
Local stateとは
一方でGlobal stateではない(Reduxの管理外であるような)stateをLocal stateと呼びます。
最もカンタンな例は、React.Componenrt
を継承したクラスを使って管理される状態でしょう。
Local stateの例
Local stateの例としては、コンポーネントに閉じるような状態が挙げられるかなと思います。
たとえば、あるコンポーネントがクリックされた回数を考えます。もしこのクリック回数という状態が、コンポーネントの中でしか使われない(たとえばrender関数内で表示するのみ)としたら、Reduxで管理せずとも良さそうです。
逆に言うと、クリック回数がアプリケーション全体に影響するならReduxに任せるべきかもしれません。
実例で見るGlobal stateとLocal stateの共存
では、ここからはGlobal stateとLocal stateの共存方法について、実例を見ていきたいと思います。
前提環境
まずは、React + Reduxによるアプリケーション作成環境を整えます。
お試しに使う程度なので、こちらのボイラープレートを使うと良いでしょう。
TrySpace/simple-redux-boilerplate
$ git clone git@github.com:TrySpace/simple-redux-boilerplate.git
内容はシンプルなカウンタアプリケーションで、最低限のRedux stateフローが作成されています。
$ git cloneが終わったら、ディレクトリをリネームしておくと良いでしょう。
$ mv simple-redux-boilerplate local-state-trial
パッケージをインストールして、npm startコマンドを打てば、開発に入ることができます。
$ cd local-state-trial
$ npm i
$ npm start
http://localhost:3000/ にアクセスすると、以下のようなアプリケーションが立ち上がります。
マイナスあるいはプラスのボタンをクリックすると、カウンタがデクリメントあるいはインクリメントされます。
Global state
Global stateについては、今回は既に作成済です。
// ./src/reducers/counter.js export default function counter(state = 0, action) { switch (action.type) { case INCREMENT_COUNTER: return state + 1; case DECREMENT_COUNTER: return state - 1; default: return state; } }
カウンタの状態に関して、Reduxが管理を行っています。なのでpropsを通じて、他のコンポーネントから現在のカウント数を参照することができます(Local stateの場合は、その子コンポーネントからのみ参照可能)。
Local state
では、Local stateを作成していきましょう。 ここでは、カウンタの表示・非表示を切り替える機能を付けてみます。表示であるか非表示であるかがここでのstateですね。
こちらが現在のカウンタコンポーネントです。
import React, { Component, PropTypes } from 'react'; export default class Counter extends Component { constructor(props, context) { super(props, context) } handleIncrement() { this.props.actions.increment() } handleDecrement() { this.props.actions.decrement() } render() { return ( <div className="counter-container"> <div className="counter-num-label">{this.props.counter}</div> <div className="counter-even-label">{this.props.counter % 2 === 0 ? 'even' : 'odd'}</div> <br /> <div className="counter-buttons"> <button onClick={() => {this.handleDecrement();}}>-</button> <button onClick={() => {this.handleIncrement();}}>+</button> </div> </div> ) } } Counter.propTypes = { counter: PropTypes.number.isRequired, actions: PropTypes.object.isRequired }
まあ、普通のReactコンポーネントですね。
ちなみに、Koba04さんの「Reactの最新動向とベストプラクティス」によりますと、最近では状態を持たないコンポーネントはStatelessFunctionalComponent(ステートレス ファンクショナル コンポーネント)を使うと良いらしいです。
import React from 'react' const Hoge = (props) => { return ( <div> {props.hoge.body} </div> ) } export default Hoge
そしてもし、コンポーネントが状態を持つようになったら、React.componentを継承したclassを使おうねとのこと。
import React from 'react' class Hoge extends React.Component { constructor(props, context) { super(props, context) this.state = { hoge: { body: '本文' } } } render() { return ( <div> {this.state.hoge.body} </div> ) } }
なので、Local stateを扱うには、StatelessFunctionalComponentではなく、React.Componentを継承したクラスを用います。
では、先程のカウンタコンポーネントに、表示・非表示のLocal stateを持たせてみます。
export default class Counter extends Component { constructor(props, context) { super(props, context) // Local stateを定義 this.state = { isVisible: true } } // 表示・非表示の切り替えアクション toggleVisibility() { this.setState({ // 表示・非表示を反転 isVisible: !this.state.isVisible }) } handleIncrement() { this.props.actions.increment() } handleDecrement() { this.props.actions.decrement() } render() { // 表示 or 非表示 const isVisible = this.state.isVisible ? 'block' : 'none' return ( <div className="counter-container"> {/* ↓ 表示・非表示stateに応じて、CSSで表示を操作 */} <div className="counter-num-label" style={{display: isVisible}}>{this.props.counter}</div> <div className="counter-even-label">{this.props.counter % 2 === 0 ? 'even' : 'odd'}</div> <br /> <div className="counter-buttons"> <button onClick={() => {this.handleDecrement();}}>-</button> <button onClick={() => {this.handleIncrement();}}>+</button> </div> {/* 表示・非表示の切り替えアクションを発行するボタン */} <button onClick={() => {this.toggleVisibility() }}> {this.state.isVisible ? '閉じる' : '開く'} </button> </div> ) } }
コード中にコメントを書きましたが、以下、個別に見ていきます。
Local state定義
contractor
関数内でLocal stateを定義します。
constructor(props, context) { super(props, context) // Local stateを定義 this.state = { isVisible: true } }
これはReact, Reduxというより、JavaScriptのclassの話ですね。class内から、this.state
の形で参照することができます。
参照: MDN - class
Local state変更アクションの定義
Local stateを変更するアクションを定義します。
// 表示・非表示の切り替えアクション toggleVisibility() { this.setState({ // 表示・非表示を反転 isVisible: !this.state.isVisible }) }
toggleVisibilityが呼ばれると、setStateが行われてカウンタコンポーネントが再レンダリングされます。
Local stateをrender関数内で利用
class内で定義したLocal stateとactionをrender関数内で利用します。
render() { // 表示 or 非表示 const isVisible = this.state.isVisible ? 'block' : 'none' return ( <div className="counter-container"> {/* ↓ 表示・非表示stateに応じて、CSSで表示を操作 */} <div className="counter-num-label" style={{display: isVisible}}>{this.props.counter}</div> <div className="counter-even-label">{this.props.counter % 2 === 0 ? 'even' : 'odd'}</div> <br /> <div className="counter-buttons"> <button onClick={() => {this.handleDecrement();}}>-</button> <button onClick={() => {this.handleIncrement();}}>+</button> </div> {/* 表示・非表示の切り替えアクションを発行するボタン */} <button onClick={() => {this.toggleVisibility() }}> {this.state.isVisible ? '閉じる' : '開く'} </button> </div> ) }
コードについては以上です。 コンポーネント内に閉じたstate及び、actionを作成することができました。要は素のReactの世界ですね。これがReduxのGlobal stateと共存しています。ポイントは、お互いが干渉すること無く共存している点ですね。
挙動
きちんとGlobal state(カウンタのインクリメント・デクリメント)と、Local state(カウンタの表示・非表示)が共存しているのがわかります。
状態管理をRedux外に漏らすことについて
Reduxの公式FAQに、以下のような項目があります。
Do I have to put all my state into Redux? Should I ever use React's setState()?
アプリケーションの状態管理を全てのReduxに任せるべきか、あるいはReactのsetStateも使っていくべきなのか。というものです。
返答としては、以下の通り。
There is no “right” answer for this. (中略) Find a balance that works for you, and go with it.
正しい答えはないので、良い塩梅を自分で探ってみてね。とのこと。 Reduxサイドとしても「全ての状態管理をReduxで行うべき」とは考えていないようです。
2016/09/29 追記:
Reduxの作者のDan AbramovがReduxの用法についてMedium.comに記事を投稿していました。
You Might Not Need Redux - Medium.com
Reduxを使わずともReduxのアイデアを取り入れることはできるし、 Local state is fine.
とのことです。
Local stateを実現するツール
先程のリンクの中で、Local stateを実現するためのツールがいくつか挙げられています。
ただしどれもdecoratorを使う必要があるので、慣れていない人は今回紹介したような手法でもいいのかなと思います。
Local stateを使うべき時
では、どんなときにLocal stateを使うべきなのでしょうか。 個人的には、UIに関わる状態はLocal stateに向いていそうだなと思います。例としては、
- 上下キーによるセレクトボックスのカーソル移動
- レビューの評点スターhoverイベント
などが、コンポーネントに閉じたstateとして適していそうです。以下は、転職会議のUIイベントをスクショしたものです。
これはUI上の描画に関わる状態であり、Local stateとしてコンポーネント内で利用するのが良いでしょう。
一方で、ドメインやビジネスロジックに関わる状態は、ReduxでGlobal stateとして管理するのが良さそうです。
また、Local stateとして定義したものが、実はGlobal stateとして定義するべきだった…ということもあるかもしれません。状態がGlobalで持つべきものなのかLocalで持つべきものかについては、チーム内で話し合うと良いかもしれません。
締め
今回はReduxでGlobal stateとLocal stateを共存させる方法について書かせて頂きました。
Global state, Local stateと偉そうな名前を使いましたが、要はReduxと素のReactを一緒に使っているだけですね。ただし、Redux初心者の自分としてはなるほどなあという気持ちでした。
適宜Local stateを用いることで、Reduxの難点の1つであるState, Reducerの肥大化を抑制できるかもしれませんね。 React・Reduxアプリケーションを作成する上で、参考になりましたら幸いです。