はじめに
こんにちは、nukkyです。
今回は初心者(私)による初心者の為のReduxの基本について備忘録がてらまとめていきます。
Reduxとは
そもそもReduxってなに?
Reduxは、Fluxアプリのデザインパターンにヒントを得て、JavaScriptアプリでStateを管理するために設計されました。ReduxはReactと併用されることが多いのですが、ReduxはjQueryやAngularといった別のフレームワークと併用することもできます。
Reduxのサイズは非常に小さいにもかかわらず、アプリの各コンポーネントが自分のStateに直接アクセスできるようになります。このとき、子コンポーネントにpropsを送信する必要も、親コンポーネントがデータを受け取るのにコールバック関数を使う必要もありません。
Fluxとは
Fluxとは、Facebookが提唱したアプリケーションのデータフロー管理のためのアーキテクチャパターンです。Stores、View、Actions、Dispatcherなるパーツで構成されており、役割は以下の様になっています。
- Stores: アプリケーション全体のデータとビジネスロジック
- View: React等のコンポーネント
- Actions: View等から発火されて作られるイベント
- Dispatcher: 全てのアクションを受けてStoreにイベントを発火する
gitにある画像がわかりやすいのですが、以下の様な流れになっています。
全てのデータのオペレーションがDispatcherに集約されており、Store側ではDispatcherから送られてくるActionに対してデータを処理する関数を登録してあります。
すなわちActionを発行して、Dispatcherがそれを検知しStoreを更新、そしてViewが描画されると言う流れです。データフローが単一方向になることで理解しやすく、また状態を管理しやすくなります。
もし興味があればReduxの源流となるので確認してみてください。
https://github.com/facebook/flux/tree/master/examples/flux-concepts
Reduxの3原則
Reduxには、以下の3つの原則があります。
- Single source of truth (信頼できる一つの情報源)
- State is read-only (状態は読み取り専用)
- Changes are made with pure functions (変更は純粋な関数によって行われる)
それでは一つづつ見ていきます。(ここで出てくる用語の解説は後述)
Single source of truth
アプリケーション全体の状態(state)はツリーの形で1つのオブジェクトで作られ、1つのストアに保存する。
つまり状態をシングルツリー構造にすることによって、信頼性が高まり、デバッグやアプリケーションの動作の予測が容易になります。
State is read-only
状態を変更する手段は、変更内容をもったアクションオブジェクトを発行したときのみとします。これにより中央集権的に管理することが可能となり、デバッグが容易となります。
Changes are made with pure functions
ここで言う関数とは「Reducer」のこと、「Reducer」とはアクションがどのように状態を変更するかを行う関数です。
「Reducer」は状態とアクションを受けて、新しい状態を返す関数であり、ポイントとしては、現在のstateオブジェクトを変更することはせずに、新しいstateオブジェクトを作って返す。
状態の変更を統一するために、「Reducer」は外部要因に作用されない単純な関数として実装するべきです。
Reduxの要素
Reduxは異なる役割を持った以下の要素で構成されています。
- Action
- Store
- State
- Reducer
それでは一つずつ見ていきましょう。
Action
Actionは「何をする」という情報を持ったオブジェクトです。Actionはtypeプロパティを必ず持つ必要があります。
また、ActionはReactの状態を更新することのできる唯一の方法であり、以下のように定義されます。
1 2 3 4 | { type: 'ADD_TODO', text: 'Make dinner' } |
ActionCreator
ActionCreatorはActionを作成するメソッドです。これを実行することで特定のActionが生成され返却されます。
1 2 3 4 5 6 | function addTodo(text) { return { type: ADD_TODO, text } } |
Store
Storeはアプリケーションの状態(state)を保持している場所です。
Storeはアプリケーション内で一つ存在し、一つのstateを保持しています。
Storeの役割は、stateを保持することやstateへアクセスするためのgetState()、stateを更新するためのdispatch(action)、そしてリスナーを登録するためのsubscribe(listener)を提供する事です。
Storeをつくるには、combineReducerでつくられたReducerをcreateStore()へ渡します。
1 2 3 | import { createStore } from 'redux' import todoApp from './reducers' let store = createStore(todoApp) |
stateの初期値を渡したい場合にはcreateStoreの第2引数に入れます。
1 | let store = createStore(todoApp, window.STATE_FROM_SERVER) |
State
Stateとはアプリケーションの状態を表します。
以下のサンプルコードでは「現在選択されている表示/非表示」、「todoのリスト」をstateとして保持します。
1 2 3 4 5 6 7 8 9 10 11 12 13 | { visibilityFilter: 'SHOW_ALL', todos: [ { text: 'Consider using Redux', completed: true, }, { text: 'Keep all state in a single tree', completed: false } ] } |
Reducer
ReducerはActionとStateから、新しいStateを作成して返すメソッドです。三原則のところでも触れましたがポイントは、引数のStateを更新することはせず、新しいStateのオブジェクトを作成して返します。
Reducerのメソッドは副作用を起こさないpureな関数でなければならず、AというStateに対して外部要因による変更はなく、毎回必ずBというStateを返すような関数でなければなりません。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | function todoApp(state = initialState, action) { switch (action.type) { case SET_VISIBILITY_FILTER: return Object.assign({}, state, { visibilityFilter: action.filter }) case ADD_TODO: return Object.assign({}, state, { todos: [ ...state.todos, { text: action.text, completed: false } ] }) case COMPLETE_TODO: return Object.assign({}, state, { todos: [ ...state.todos.slice(0, action.index), Object.assign({}, state.todos[action.index],{ completed: true }), ...state.todos.slice(action.index + 1) ] }) default: return state } } |
このサンプルソースではswitch文でActionsのtypeを判断し、処理を行います。
この際、もし該当するtypeがなければ、Stateを変更してはいけないのでdefaultで今のStateをそのまま返すようにします。
またReducerでは直接Stateを書き換えてはいけないので、Object.assignでは最初に空のオブジェクト{}を渡して、Stateをコピーするようにしています。
Reduxでは最初にreducerはstateがundefinedで呼び出します。その際に初期値を設定します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | import { VisibilityFilters } from './actions' const initialState = { visibilityFilter: VisibilityFilters.SHOW_ALL, todos: [] } function todoApp(state, action) { if (typeof state === 'undefined') { return initialState } // For now, don’t handle any actions // and just return the state given to us. return state } |
結局Reduxって必要なの?
Reactというか他のフレームワークもですが、基本的に親子関係にないコンポーネント同士でのやりとりが色々と面倒で基本的な機能のみではスムーズにできません。Reactでは、それが必要な場合にはFluxのパターンに従ってグローバルなイベントシステムを構築せよとしています。ここがReduxの出番だと思います。
ただ開発規模や既にルール化してあるものに無理やり組み込んだりとかする必要はないと思いますので以下の様な場合はReduxでなくても良いかもしれません。
- コンポーネント間でStateの共有や加工を行う方法を既に定義済みの場合
- ReactなどのReduxを使用としているフレームワークの経験がまだ浅い場合
- シンプルなUI変更などぐらいしか行う予定がなく、Reduxストアに含める必要もなければコンポーネントレベルで扱えるようにする必要もない場合
- ビューごとにフェッチするデータソースが1つしかない場合
さいごに
Reduxは情報も多いですし私が作ろうとしているアプリを作成するのには必要かなと感じています。まだまだ勉強中の身ですが記事にまとめた事でより理解が深まったと思います、次回も是非お付き合いいただければと思います。