概要
前回までのアプリケーションに,Reduxを導入します.ReduxはFluxという概念を導入したフレームワークです.Fluxに関しては,様々な記事があるので,ググってみて下さい.Reduxはフレームワークなので,アプリケーションを実装するときに決まった手順で実装していくことになります.具体的には,Reduxでは,以下のコンポーネントが登場します.
- Store : ステートを管理するコンポーネント
- Reducer : アクションとステートを受け取り,新しいステートを作って返すコンポーネント
- Action : 任意のオブジェクト.ただし,Flux Standard Action形式に従うのが普通
- ActionCreator : アクションを生成するコンポーネント
とりあえず,この4つとそれぞれの関係を理解しておけば十分だと思う.
Redux概要(個人的な理解)
Fluxとの関係
Reduxは,Fluxを実現するフレームワークの1つである.Fluxでは,
Action->dispatcher->Store->View->Action...のように,Actionがディスパッチされれ,その結果ステートが変化し,ステートの状態を監視しているViewに変更が加えられる,という風にアプリケーションを開発・実行する.
FluxとReduxの関係を表にまとめてみた.
Flux | Redux |
---|---|
Action | Action(ActionCreator) |
dispatcher | Store.dispatch |
store | Store(Reducer) |
このように開発スタイルを決めることによって,ある現象が起きたときにどこを見ればよいかはっきりするのでバグの混入やミスを少なくできる.また,デバッグするときにも効率よく実施できる.
Reactとの関係
基本的にReduxはReactとは無関係である.Reduxは開発スタイルを統一するフレームワークで,ReactはViewのフレームワークであり,お互い基本的には関係ない.しかし,Reactだけで開発を進めると,開発規模が大きくなったときに破綻してくる.そのため,開発スタイルを統一するReduxを導入したほうが良い,ということになる.これは,GUIアプリケーション開発でMVCアーキテクチャを導入したほうが良い,という関係と同じである.
そして,ReactはFluxのViewの役割を果たすフレームワークで,ステートの変更によって必要なコンポーネントが再描画されるが,ステートはComponentを継承するクラスコンポーネント(関数コンポーネントはステートを持たない)がそれぞれ持っていて,どのコンポーネントが持つステートが変更されると,どのViewコンポーネントが変更されるのか変更されるのか認識していないといけないため,規模が大きくなると処理が煩雑になる.そのため,ステートを一元的に管理し,どのコンポーネントも1つのステートを監視する,という方針でアプリケーションを構築するのがReduxである.そして,そのステートを管理し,dispatchメソッドを備えるのがStoreクラス,となっている.
よって,アプリケーション全体に関係するようなステートはすべてStoreで管理すべきである.一方,各コンポーネントのステートはStoreとは独立しているので,各コンポーネントで個々にステートを使うこともできる.この使い分けは,あるViewコンポーネントに限定した処理を行いたい(例えばUIだけの変更)場合は各コンポーネントのステートを用い,アプリケーション全体に関わるものはStoreで管理すればよいのだろう.
準備
Reduxをインストールします
>npm install --save redux
前回作ったアプリケーションで,Input3
を使ったやり方をベースにReduxを適用していく.
Reduxを利用する場合,上に示したStore, Reducer, Action, ActionCreatorを作る必要があります.加えて前回作ったアプリケーションもいくつかファイルがありますので,ここでディレクトリ構造を整理する.
src/
├ index.js // アプリケーションのエントリーポイント.今回は少し修正します.
├ App.js // メインコンポーネント.ItemListやInput3などのコンポーネントを含む.
├ Item.js // 個々のアイテムを表現する.今回は修正なし
├ ItemList.js // アイテムのリストを表現する.今回は修正なし
├ Input3.js // 入力フォームを生成します.今回は修正します.
├ actions/
├ item.js // 新規作成.ActionCreatorを定義します.
├ reducers/
├ item.js // 新規作成.Reducerとステートを初期化します.
これらの修正を以下の手順で行う.
1. Reducerの作成
2. ActionCreatorの作成
3. Input3の各コンポーネントのイベントハンドラの修正.
4. Appの修正
5. index.jsの修正
Reduxの適用
1. Reducerの作成
Reduxでは,Storeがアプリケーション全体のステートを管理します.そして,何らかのイベントが発生してステートを更新する時には,Reducerが新たなステートを生成します.したがって,Storeは利用するReducerを知る必要があるため,Storeの生成にはReducerが必須になります.ReducerはJavaScriptの関数です.引数にstore
とaction
をとります(両者ともJavaScriptの単なるオブジェクトです).また,これ以外にも引数が取れますが,ここでは割愛します.
まず,reducers
ディレクトリを生成し,item.js
を生成してreducer
を定義します.
const initialState = { // (1)Storeが管理するステートの初期化
task: '',
tasks: [],
};
function appReducer(state = initialState, action) { // (2) reducerの定義
switch(action.type) {
case 'INPUT_TASK':
return {
...state,
task: action.payload.task,
};
case 'ADD_TASK':
const nl = state.tasks.concat([action.payload.task]);
return {
...state,
tasks: nl
};
default:
return state;
}
}
export default appReducer;
(1)
Storeが管理するステートの初期値を定義する.ここでは,入力フォームの文字列の状態を管理するtask
と,追加されるtask
のリストを管理する.
(2)
addReducer
がReducerの関数となる.引数にstate
とaction
をとる.後述するように,action
はJavaScriptのオブジェクトですが,Flux Standard Action形式のオブジェクトを前提としている.
Reducerの役目は,新しいステートオブジェクトを生成して返すことである.新しいステートオブジェクトを返すために,どんなアクションを受け取ったか,また直前のステートを利用する.したがって,Reducer本体は巨大なswitch-case
文となり,この分岐にaction
の種類を利用する.例えば,INPUT_TASK
の場合には,現在のステートに加え,受け取ったaction
のtask
を使って新たなtask
を上書きし,新しいステートオブジェクトを返している.ここで,...state
という表現は,ES6から採用されたスプレッドオペレータという新しい演算子で,オブジェクトや配列の中身を展開する.ここではstate
は直前のステートを表しているので,そのメンバーはtask
とtasks
であり,これは以下のようなオブジェクトで表現されている.
{
task: xxx,
tasks: [x,x,x]
}
これをスプレッド演算子...state
を使うと
task: xxx,
tasks: [x,x,x]
と展開され,更に{}
で新たなオブジェクトを生成するので
{
task: xxx,
tasks: [x,x,x]
}
となるが,ここでtask:action.payload.task
を追加するので,
{
task: xxx,
tasks: [x,x,x]
task: action.payload.task
}
となり,最後に追加したメンバーが既存のメンバーを上書きするので,最終的に
{
tasks: [x,x,x]
task: action.payload.task
}
という新しいオブジェクトが生成され返される,という仕組みになっている.
(2)の内容はここではまだ全部説明できないが,とりあえずReducerを作るだけなら,default: return state
だけの関数を定義しておけばよい.そして,新たにAction
を定義したときにそのtype
に合わせたステートを生成して返すように修正していけば良い.
2. ActionCreator/Actionの生成
ActionCreator
は,Reducer
が受け取るAction
を生成する関数である.Reducer
やAction
がReduxの定めたルールに従うために生成する関数やオブジェクトであるのに対し,ActionCreator
は任意のJavaScript関数である.よって, Action
を返す関数であれば,名前や引数の数に制約はない.しかし,規模が大きくなると破綻してくるので,actions
ディレクトリを生成し,item.js
を生成してこの中に定義する.Reducerの定義とファイル名を同じくするのは,実はreducer
はcombineReducers
という仕組みで,複数のReducerを組み合わせることができ,そのときどのActionCreatorがどのReducerと関連があるのか,ということを識別しやすくするためである.
ここでは,文字が入力される時と,追加ボタンが押された時,の2つのイベントを処理するため,ActionCreator
を2つ生成する.
export const inputTask = (task) => { // (1) 文字を入力されるイベント
return ({
type: 'INPUT_TASK',
payload: {
task
}
});
}
export const addTask = (task) => { // (2)アイテムを追加するイベント
return ({
type: 'ADD_TASK',
payload: {
task
}
});
}
(1)
これは,input
のonChange
イベントで実行されることを想定している.引数はテキストフィールドに入力された文字列全体を受け取る.そして,アクションを生成して返す.ここで定義したアクションの形式が.Flux Standard Actionである.
{
type: some_type,
payload: {
}
}
これはJavaScriptのオブジェクトで,2つのメンバーから構成されている.'type'でアクションのタイプを表し,その内容は'payload'というメンバーに任意のオブジェクトとして定義できる.ここでは,実はpayload
は全く同じで,単にtype
だけが異なる2つのActionCreator
を定義している.このアプリケーションではとても簡単な定義になっているが,実際のアプリケーションではこのActionCreator
にビジネスロジックを書くことになる(と思う).すなわち,何らかのデータをコンポーネントがProps
経由で受け取り,ActionCreator
を必要な引数を与えて呼び出す.ActionCreator
は受け取った引数などから適切なビジネスロジックを実行し,結果をAction
に格納してReducer
に与える,という感じだろうか.
(2)
これは,button
のonClick
イベントで実行されることを想定している.あとは(1)と同様なので割愛.
3. Input3.jsの修正
2で定義したActionCreator
を実行するように,Input3
を修正する.具体的には,イベントハンドラやその中で実行する処理をActionCreator
を実行するように変更する.
import React, {Component} from 'react';
import {inputTask, addTask} from './actions/items';
class Input3 extends Component {
constructor(props) {
super(props); //(1)ステートは削除
this.addItem = this.addItem.bind(this);
}
addItem() {
const {task} = this.props.store.getState();
this.props.store.dispatch(addTask(task)); // (2)addTaskを実行するように変更
}
render() {
return (
<div>
<input type="text" onChange={(e) =>
{this.props.store.dispatch(inputTask(e.target.value))}} /> // (3)inputTaskを実行するように変更
<button onClick={this.addItem}>Add</button>
</div>
)
}
}
export default Input3;
(1)
まず,前回このコンポーネントで管理していたステートを削除する.今回はすべてStore
で管理するためである.
(2)
ボタンを押されたときに実行する処理を変更する.まず,これまではInput3
内部でステートを管理し,追加する文字列を取得していたが,Reduxの導入でその情報はStore
で管理することにした.そのため,this.props.store.getState()
でStore
からステートを取り出している.次に,追加された文字列(task)を使ってaddTask
を実行している.この返り値はAction
であり,これを使ってthis.props.state.dispatch
を実行している.このdispatch
は,
をStoreに届ける仕組みであり,
ActionFlux
のdispatch
と同義である.dispatch
が実行されると,Reducer
が実行され,新たなステートを生成する,という流れになる.ここで,this.props.store
はどこからやってくるのか疑問だと思うが,これは親のコンポーネントからProps
経由で渡さなければいけない.詳しくは変更後のApp
を参照.
(3)
(2)と同様にActionCreator
を実行するように変更する.なお,ここではイベントハンドラを設定するのではなく,クロージャを直接定義して設定している.
4. Appの修正
import React, { Component } from 'react';
import './App.css';
import Input3 from './Input3';
import ItemList from './ItemList';
class App extends Component {
constructor(props) {
super(props); // (1) ステートの削除
}
render() {
return (
<div>
<h1>Todo App</h1>
<Input3 store={this.props.store}/> // (2)Input3にstoreを渡す
<ul>
<ItemList items={this.props.store.getState().tasks} /> // (3)ItemListにtasksを渡す
</ul>
</div>
);
}
}
export default App;
(1)
まず,ステートの管理はStore
で行うため,ステートの初期化を削除する.
(2)
Input3
ではstore
を使う必要があるため,props
経由で渡す.
(3)
これまではApp
のステートでitems
を管理していたので,それをStore
のステート経由で渡すように変更する.
このように,Reduxを導入するとApp自身には状態を保持する必要がなくなる.したがって,関数コンポーネントで定義もできる.また,Appでもthis.props.store
と,自身のProps
からstore
を取り出しているが,これは当然上位のコンポーネント(index.js
)からProps
経由で渡されてくる.
5. index.jsの修正
これまで作成したコンポーネントをつないでいくのがindex.js
の役割となる.
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import appReducer from './reducers/items'; // (1)reducerのインポート
import App from './App';
import {createStore} from 'redux'; // (2)reduxからcreateStoreのインポート
const store = createStore(appReducer); // (3)storeの生成
//ReactDOM.render(<App />, document.getElementById('root'));
function renderApp() {
ReactDOM.render(<App store={store} />, document.getElementById('root')); // (4)Appにstoreを渡す
}
store.subscribe(()=>renderApp()); // (5)再描画の設定
renderApp(); // (6)描画の実行
(1)
Reducerをインポートしている.
(2)
Store
を生成するためのcreateStore
関数をRedux
からインポートする.
(3)
appReducer
を引数に,Storeを生成する
(4)
App
に生成したstore
をProps
経由で渡す.ここから芋づる式にstoreを子,孫コンポーネントに渡していく.
(5)
store
にはsubscribe
というメソッドがあり,これはstore
の管理するStateが変更されたときに実行するコールバック関数を設定する.実はReducer
で新しいステートを返した後,setState
は明示的に呼んでいない.ReactではsetState
を呼び出して新たなステートを設定しない限り再描画されない.そこで,このコールバック設定することで,store
のステートが変更された時に呼び出されるようになり,コールバックでrenderApp
を呼び出すことで全てのコンポーネントのrender
が呼び出されるようになる.
以上の修正によって,これまでのアプリケーションにReduxを組み込むことができた.しかし,今回の修正でわかったように,全てのコンポーネントでstoreが必要になるため,Props
で引き継いていかなければならない.かつ,アプリケーションが本来の関心事とReduxを使うためのコードが混在してしまっている.そこで次に導入するのがReact-Reduxライブラリである.次は,このアプリケーションをさらに改良し,React-Reduxライブラリを適用していく.次回に続く