LoginSignup
4

More than 5 years have passed since last update.

React-Redux超入門(3)

Last updated at Posted at 2018-08-07

概要

前回までのアプリケーションに,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の関数です.引数にstoreactionをとります(両者ともJavaScriptの単なるオブジェクトです).また,これ以外にも引数が取れますが,ここでは割愛します.
まず,reducersディレクトリを生成し,item.jsを生成してreducerを定義します.

reducers/item.js
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の関数となる.引数にstateactionをとる.後述するように,actionはJavaScriptのオブジェクトですが,Flux Standard Action形式のオブジェクトを前提としている.
Reducerの役目は,新しいステートオブジェクトを生成して返すことである.新しいステートオブジェクトを返すために,どんなアクションを受け取ったか,また直前のステートを利用する.したがって,Reducer本体は巨大なswitch-case文となり,この分岐にactionの種類を利用する.例えば,INPUT_TASKの場合には,現在のステートに加え,受け取ったactiontaskを使って新たなtaskを上書きし,新しいステートオブジェクトを返している.ここで,...stateという表現は,ES6から採用されたスプレッドオペレータという新しい演算子で,オブジェクトや配列の中身を展開する.ここではstateは直前のステートを表しているので,そのメンバーはtasktasksであり,これは以下のようなオブジェクトで表現されている.

state
{
  task: xxx,
  tasks: [x,x,x]
}

これをスプレッド演算子...stateを使うと

...state
task: xxx,
tasks: [x,x,x]

と展開され,更に{}で新たなオブジェクトを生成するので

{...state}
{
  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を生成する関数である.ReducerActionがReduxの定めたルールに従うために生成する関数やオブジェクトであるのに対し,ActionCreatorは任意のJavaScript関数である.よって, Actionを返す関数であれば,名前や引数の数に制約はない.しかし,規模が大きくなると破綻してくるので,actionsディレクトリを生成し,item.jsを生成してこの中に定義する.Reducerの定義とファイル名を同じくするのは,実はreducercombineReducersという仕組みで,複数のReducerを組み合わせることができ,そのときどのActionCreatorがどのReducerと関連があるのか,ということを識別しやすくするためである.
ここでは,文字が入力される時と,追加ボタンが押された時,の2つのイベントを処理するため,ActionCreatorを2つ生成する.

actions/items.js

export const inputTask = (task) => { // (1) 文字を入力されるイベント
    return ({
        type: 'INPUT_TASK',
        payload: {
            task
        }
        });
}

export const addTask = (task) => {  // (2)アイテムを追加するイベント
    return ({
        type: 'ADD_TASK',
        payload: {
            task
        }
    });
}

(1)
これは,inputonChangeイベントで実行されることを想定している.引数はテキストフィールドに入力された文字列全体を受け取る.そして,アクションを生成して返す.ここで定義したアクションの形式が.Flux Standard Actionである.

FluxStandardAction
{
  type: some_type,
  payload: {
  }
}

これはJavaScriptのオブジェクトで,2つのメンバーから構成されている.'type'でアクションのタイプを表し,その内容は'payload'というメンバーに任意のオブジェクトとして定義できる.ここでは,実はpayloadは全く同じで,単にtypeだけが異なる2つのActionCreatorを定義している.このアプリケーションではとても簡単な定義になっているが,実際のアプリケーションではこのActionCreatorにビジネスロジックを書くことになる(と思う).すなわち,何らかのデータをコンポーネントがProps経由で受け取り,ActionCreatorを必要な引数を与えて呼び出す.ActionCreatorは受け取った引数などから適切なビジネスロジックを実行し,結果をActionに格納してReducerに与える,という感じだろうか.

(2)
これは,buttononClickイベントで実行されることを想定している.あとは(1)と同様なので割愛.

3. Input3.jsの修正

2で定義したActionCreatorを実行するように,Input3を修正する.具体的には,イベントハンドラやその中で実行する処理をActionCreatorを実行するように変更する.

Input3.js
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は,
Action
をStoreに届ける仕組みであり,Fluxdispatchと同義である.dispatchが実行されると,Reducerが実行され,新たなステートを生成する,という流れになる.ここで,this.props.storeはどこからやってくるのか疑問だと思うが,これは親のコンポーネントからProps経由で渡さなければいけない.詳しくは変更後のAppを参照.

(3)
(2)と同様にActionCreatorを実行するように変更する.なお,ここではイベントハンドラを設定するのではなく,クロージャを直接定義して設定している.

4. Appの修正

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の役割となる.

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に生成したstoreProps経由で渡す.ここから芋づる式にstoreを子,孫コンポーネントに渡していく.

(5)
storeにはsubscribeというメソッドがあり,これはstoreの管理するStateが変更されたときに実行するコールバック関数を設定する.実はReducerで新しいステートを返した後,setStateは明示的に呼んでいない.ReactではsetStateを呼び出して新たなステートを設定しない限り再描画されない.そこで,このコールバック設定することで,storeのステートが変更された時に呼び出されるようになり,コールバックでrenderAppを呼び出すことで全てのコンポーネントのrenderが呼び出されるようになる.

以上の修正によって,これまでのアプリケーションにReduxを組み込むことができた.しかし,今回の修正でわかったように,全てのコンポーネントでstoreが必要になるため,Propsで引き継いていかなければならない.かつ,アプリケーションが本来の関心事とReduxを使うためのコードが混在してしまっている.そこで次に導入するのがReact-Reduxライブラリである.次は,このアプリケーションをさらに改良し,React-Reduxライブラリを適用していく.次回に続く

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4