目次
- 今から始めるReact入門 〜 React の基本
- 今から始めるReact入門 〜 React Router 編
- 今から始めるReact入門 〜 flux編
- 今から始めるReact入門 〜 Redux 編: immutability とは ←★ここ
- 今から始めるReact入門 〜 Redux 編: Redux 単体で状態管理をしっかり理解する
- 今から始めるReact入門 〜 Redux 編: Redux アプリケーションを作成する
- 今から始めるReact入門 〜 Mobx 編
概要
Redux はJavaScript アプリケーションのための状態管理コンテナです(WordPress フレームワークにもRedux Framework というものがありますが、そちらではありません)。
そもそも状態管理とは何?という方はすでに説明済みですので前回のflux の記事を参照してください。
SPA(Single Page Application) が出てきてからフロントエンド側での状態管理は重要性を増してきており、且つ状態管理を実現するためにフロントエンド側のコードが益々複雑になりメンテナンスしにくい、またテストしにくいという悩みがついてまわります。
そこでRedux を使うことにより安定して一貫性のある動作の実現とテストタブルなコードが作成でき、結果としてSPA に対する機能の追加を簡単にし、バグの発見もしやすくなります。
また、JavaScript の特徴として非同期処理が所々でてきますがRedux のMiddlewares という概念も利用することで、非同期処理の実行とその処理結果のミューテーションの両立をしながら状態管理をするという難しい問題に対応することもできます。
今回の勉強ではRedux をReact と組み合わせて使っていきますが、Redux はReact とは切り離され提供されているためReact はもちろんのこと、React 以外のView ライブラリと組み合わせて使うこともできます。
そしてまずはRedux が担う役割と特徴をしっかり理解するために、最初の方はRedux 単体で動きを見ていき、そのあとReact と組み合わせてアプリケーションを作成していくように進めていきます。
Redux の構造
Redux を図示すると次のようになります。
+---------------------------------------------------+
| |
+- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -+
| | | |
| ↓
| +-+-------------+ +---------+ +-+-----------------------+ |
| Actions | | API |<-------| Middlewares |
| +-+-------------+ | |------->| | |
↑ +---------+ +-| Dispatcher(middlewares) |-+
| | | +-+-----------------------+ | |
+- - - - - - - - - - - - - - - - - - - - - - - -+ | | |
| | | | One Store +---+ | |
| | ↓ ↓ | |
| | | +--------------------+ | | | Redux area
| | | Reducer(s) | | |
| | | +-+------------------+ | | |
+--+---------------------+ | | | |
| View (Rendered state) | | | | | | |
| +--------------------+ | | ↓ | |
| | Presentational | | | | +--------------------+ | | |
| | Components | | | | State +-+ |
| +-+------------------+ | | +-| |------+ |
| ↑ | +----------+---------+
| | | | | |
| Pass Data as Props | |
| | | +- - - - - - - - -|- - - - - - - - - - -+
| +-+------------------+ | Re-Render |
| | Container |<----- when -------------------+
| | Components | | Store Changes
| +--------------------+ |
+------------------------+
// Provider Component, Smart Components, Dumb Components はReact 内の要素
各要素の簡単な説明は次の通りです。
One Store
Redux のStore の特徴として、flux とは異なり、1 つのStore のみ存在することです。
すべての状態データを一箇所で管理することになるのでシンプル性を維持することができます。
画面及びアプリケーションの初期化はStore から始まります。
例えばTODO アプリケーションのStore を例に考えるとflux 的な思想ではTODO list store
, Setting store
, favorites store
等のstore が必要なコンポーネントとして上がってくるかもしれませんが、Redux ではそれらを1 つの巨大なObject に入れて管理します。
またRedux のStore の重要な性質としてimmutable であり、一度作成されてStore に格納されたデータは外からの手によって直接変更されることは無く、データは常に新く生成されたデータを置き換える形で更新させます。
注意点として、JavaScript はネィティブでimmutable は性質を持っていない(これらについては後ほど言及します)ため、Store のデータを更新する場合はJavaScript で擬似的にこの性質を実現させる必要があります。
この方法については後ほど説明をしていきます。
State
One Store の中に存在し、アプリケーションの様々な要素の状態です。
State はブラウザのローカルストレージ内に持たせたりすることもできますが、Redux においてはState を一つのJavaScript オブジェクト内に保管します。
そしてそのオブジェクト内には例えば現在どのページが表示されているのか、どのアイテムが設定されているのか、どのユーザで操作しているのかといったState を保持しています。
状態(State)らは、正規化/非正規化しても大丈夫で、JSON のようなもので状態を保存して別のブラウザでその状態をロードすれば同じアプリケーション(同じページ、同じアイテム、同じユーザ)がレンダリングされます。
例えばカウンターアプリケーションでは、State は次のようなJSON 形式で保存されます。
var initialState = initialState = {counter: 0};
View (Renderred state)
状態をレンダリングします。
今回の勉強ではReact がそれに該当しますが、その他のView フレームワークでも可能ですし、plain なJavaScript でも可能です。
例えばplain なJavaScript で状態をレンダリングする場合、以下のようにHTML ファイルを用意し、そこに対してJavaScript からDOM をレンダリングしてあげることになります。
<div id="counter"></div>
function render(state) {
document.getElementById('counter').textContent = state.counter;
}
もちろんReact と組み合わせた場合はこの記述はReact の書き方で置き換わることになります。
Presentational Components とContainer Components
React の領域に当たります。
今回はRedux の範疇からそれてしまうため、説明を割愛します。
Actions
アプリケーション状態が変更される時、Action が発動します。
Action の種類としては、ユーザのアクション、非同期アクション、スケジュールされたアクションなどがあります。
わかりやすい例としては画面にボタンを表示することで、ボタンが押されたタイミングでAction を発動する等です。
<button id="button">Increment</button>
document.getElementById('button').addEventListener('click', incrementCounter)
Store and Reducer
Actions は状態を直接変更することはありません。Redux のstore がその役割を担います。
Store に対してAction データを送出するときはdispatch を行います。
var store = Redux.createStore(reducer, initialState);
function incrementCounter() {
store.dispatch( /* (1) */
type: 'INCREMENT'
)
}
Redux Store は現在の状態を保持し、Action に対して反応します。
Action がdispatch された時(上記コードの(1) の部分)、Store は現在のstate と現在のAction データから必要に応じて計算を行い、state を決定します。
またこの時、サーバや外部API 等との通信が必要な場合はそれを非同期処理を使って行い、それが終わるのに時間がかかる場合は"処理中" のようなstate をひとまずは発行し、API からのコールバックによって"処理完了(場合によってはエラー)" といったstate を作成するようにすることができます。
そしてreducer が返却した値によってstate は更新されます。
function reducer(state, action) {
if (action.type === 'INCREMENT') {
state = Object.assign({}, state, {counter: state.counter + 1})
}
return state
}
state が変更されたら
state が変更されたら、直ちに画面の再レンダリングを行います。
store.subscribe(function() {
render(store.getState())
})
React.js を使えば、まさにその名のとおりであるrender() メソッドをReact コンポーネントは持っているので、それを呼び出せばOK ということです。
Redux は何の問題を解決するか
典型的なJavaScript プログラム内ではState でいっぱいで、例えばState の例としては以下のようなものがあります。
- ユーザが何のデータを見ているか
- どのデータを取得しているか
- どのURL をユーザに見せているか
- どのアイテムがページ内で選択されているか、件数は何件か
- エラーはあるか?そしてどのような状態か
State はJavaScript のあらゆるところにあり、単純なオブジェクトの中にState は保持されています。
実際React 自体もComponent は単純なオブジェクトでState を保持しており、setState() メソッドを使ってReact component が持つState を操作していました。
しかしアプリケーションが複雑になり幾つもの状態を管理する必要がでてきた場合、状態を遷移させたり戻ったりさせることで、ちょっとしたTODO リストアプリケーションでさえ状態を管理できなくなる可能性があります。
膨大なReact コンポーネントを誰が望んでいるのでしょうか?
React 単体のフロントエンドは複雑な状態管理といったビジネスロジックを持たせるべきではないでしょう。
React コンポーネントの代わりとして状態を管理するのは何が代わりになるでしょうか?
その代わりになるもののひとつとしてRedux があります。
最初のうちはRedux がよくわからない、何の問題を解決しているのかわからないように見えるかもしれませんが、各React コンポーネントに必要なstate を正確に与えることにRedux は優れているのです。
Redux は一箇所で状態を保持します。
また、Redux を使うことでstate を取得して管理するためのロジックをReact の外に出すことが可能になります。
このアプローチによる利点はそれほど明らかなものでは無いかもしれませんが、これから始めるRedux に足を踏み入れるとすぐにわかるようになります。
immutability なJavaScript
それでは早速Redux を触っていきましょう…と行きたいところですが、Redux の状態を安全、確実に管理するためにJavaScript におけるimmutability について理解する必要があります。
immutability という用語自体は特に新しい用語ではありませんが、Redux のStore の特性を理解するためにもこれらの動きを実際に触って見ていったほうが良いでしょう。
JavaScript は標準でimmutability なコードを書くにはあまり気の利く言語ではありません。
immutability とは簡単に言うと我々の手によって変化させられることの無い性質のことで、新しいデータをnew する(配列やオブジェクトを完全に新しく生成する)ことでのみデータを入れ替えることができます。
Imutable JavaScript
JavaScript の基本的な機能を工夫して組み合わせ、JavaScript でimmutable なデータを扱う方法について練習をしていきます。
データを変更されたくない場合はその値を持つ新しいデータの部品(object)を作れば良く、もしデータを変更したい場合はその部品の中の値を変更するのではなく、部品そのものを新しく作成して取り替えてしまえば良いという考え方ともいえます。
実際にそれらを実感するために、ターミナルからNodeを対話モードで起動してやっていきましょう。
$ node
>
プロンプトが起動したら、以下のようにname とその値を格納するObject を作成してみましょう。
> var a = {name: "Foo"};
undefined
次にこの値"Foo" を"Bar" 変更するために、変数a のname に値を代入してみましょう。
> a.name = "Bar";
'Bar'
> a
{ name: 'Bar' }
すると、a のname が"Foo" ではなく"Bar" になりますが、{name: "Foo"}
はObject は変更され、失われてしまい、/。この状態はimmutable ではありません。
JavaScript ではこのようにimmutablity がデフォルトで無いのか、ということについてはJavaScript が大きく分けて2 つのデータ型のみ持っていることに起因しています。
その 2 つのデータ型とはプリミティブ(primitive)型と参照(reference)型、です。
primitive 型とは数字、文字列、bool、その他にはnull, undefined, symbol(ECMAScript 2015〜)等です。
1 "test" true undefined
一方で参照型としては、Object, 配列等です。
{name: "foo", age: 12} ["foo", "bar", "baz"]
ここでもう一度以下のようなコマンドを実行してみましょう。
> var a = {name: "Foo"};
undefined
> var b = a;
undefined
> b.name = "Bar";
'Bar'
> a
{ name: 'Bar' }
上記の結果を見ると、変数a にObject を格納後、変数b に変数a を代入していますが、この時点で変数a が参照しているObject と変数b が参照しているオブジェクトは同じものとなります。
では次はObject.assign()
を使用して代入してみましょう。
Object.assign()
の使い方は第1 引数にベースとなるオブジェクトを指定し、第2 引数以降に変更したいデータを指定します。
> var a = {name: "Foo"};
undefined
> a = {name: "Foo", age: 35};
{ name: 'Foo', age: 35 }
> var b = Object.assign({}, a, {name: "Bar"});
undefined
> b
{ name: 'Bar', age: 35 }
> a
{ name: 'Foo', age: 35 }
すると、最終的に変数a のオブジェクトの変更無しに変数b のオブジェクトを変更することができました。
var b = Object.assign({}, a, {name: "Bar"});
の一行で、第1 引数の空のObject に対して第2 引数の変数a のオブジェクトの値、第3 引数の新オブジェクトの値({name: "Bar"}
) でオブジェクトの値をオーバライティングしていきます。
そして、最終的な結果は変数a {name: 'Foo', age: 35}
, 変数b {name: 'Bar', age: 35}
という結果になります(変数a に対しては何も触れていないので、そのままということです)。
このような性質はObject だけでなく、配列に関しても同様です。
Array.prototype.concat()
は配列に他の配列や値を繋いで、新たな配列を返します。
> var a = [0, 1, 2];
undefined
> var b = a.concat(3);
undefined
> a
[ 0, 1, 2 ]
> b
[ 0, 1, 2, 3 ]
配列a を最初に作成し、次のa.concat(3);
で配列a と値は同じである新しい配列が生成されます。
変数b には新しい配列が代入され、変数a に関してはノータッチのまま、配列が変更されること無く残り続けることになります。
次に示すArray.prototype.filter()
も同様です。
> a
[ 0, 1, 2 ]
> var b = a.filter((val) => val !== 2)
undefined
> b
[ 0, 1 ]
> a
[ 0, 1, 2 ]
上記コマンドは配列a ([ 0, 1, 2 ]
) から、filter で値が2 以外のものを抽出して、新しい配列を生成して変数b に格納します。
そして当然ながら変数a に格納されている配列は変更なしです。
次にこれよりも少し複雑なデータ構造の場合について見ていきましょう。
> var a = {name: "Foo", things: [0, 1, 2]};
undefined
> var b = Object.assign({}, a, {name: "Bar"});
undefined
> b.things = a.things.concat(3);
[ 0, 1, 2, 3 ]
> a
{ name: 'Foo', things: [ 0, 1, 2 ] }
> b
{ name: 'Bar', things: [ 0, 1, 2, 3 ] }
上記を見るとObject のプリミティブ型(String のname)と参照型(配列のthing)な値をそれぞれ変更しています。
しかし、元の変数a はどちらの値も変わらずにそのままです。
このようにプリミティブ型対しては単純な代入、参照型に対しては新しいObject または配列を作成する関数等を使用して変更してあげることでJavaScript でのimmutability は実現します。
immutability を保つのに役立つ、参照型に対して新しいObject または配列を作成する関数としては代表的なものとして以下のようなものがあります。
Object.assign(), Array.prototype.concat(), Array.prototype.filter(), Array.prototype.map(), Array.prototype.reduce()
参照型に関しては上記のような関数利用のルールを徹底することで、JavaScript のimmutability を実現することができるようになります。
次からは実際にRedux を使って簡単なアプリケーションを作成していきますが、ここで学習したimmutability は、Store を管理する特性を理解するのに役に立ちます。
spread 演算子を使う方法
Object のimmutability の手助けをする記法としてECMAScript 6 で出てくるspread 演算子を使う方法もあります。
書き方は以下のように変わります。
var a = {name: "Foo", age: 35};
var b = {...a, name: "Bar"};
Immutable JS を使う
今回は特に深くは触れませんが、可読性も意識しながらコードを組みたい場合はimmutable-js を使うのも手です。
参考
-
Redux Tutorial #1 - React js tutorial - How Redux Works
-
Immutable JS - Redux Tutorial #2 - React.js Tutorial
-
Docs need one or more diagrams #653