2019/12/10更新

[フロントエンド] Reduxの考え方をシンプルに理解しよう(入門記事)

このエントリーをはてなブックマークに追加      

こんにちは、@yoheiMuneです。
Reduxという考え方は、React、Vue、Swift、Kotlin、とクライアントアプリ界隈で非常に流行っています。今日はその考え方をシンプルに学びたいと思い、ライブラリを使わずにReduxとは何かを説明したいと思います。
Reduxの全体像


目次




この記事の目的

この記事では、Reduxを初めて学ぶ人や、一度は触れたけど挫折した人向けに、Reduxとはどのようなものかを解説しています。僕自身、いろいろな案件でReduxを使っていますが、少しもやっとしたところもあり、整理したいなと思い執筆しました。

Reduxはデータを扱うための考え方の1つです。Reduxという考え方を実現したライブラリが存在します(reduxやreact-reduxなど)。ただ、考え方を学ぶ上でライブラリを使うとわかりづらい点があります。ライブラリの中でいい感じに処理してくれる(=処理が隠蔽されている)部分があるためです。そのため、今回はあえて、ライブラリを使わずに、素のJavaScriptを実装しながらReduxの考え方を学びたいと思います。

なお、Reduxのライブラリは、コンパイルすると2KBほどの小さなライブラリで、コード全量を読むこともそんなに大変ではありません。この記事を読んだ後に、気になる方はライブラリのコードも覗いてみてください。この記事で見た用語や関数名を見つけることができ、理解が進むと思います。



Reduxを構成する要素

前述でも触れましたが、Reduxはデータを扱う考え方の一つです。Reduxでは、アプリケーションで扱うデータ全てを、1つの変数(stateと呼びます)で管理します。

Reduxのstateのイメージ
その、ステート(state)を管理するために、ストア( store)というものがあり、ステートを変更する際に、アクション(action)、ディスパッチ(dispatch)、リデューサー(Reducer)、というものがあります。ステートの変更を受け取るために、サブスクライブ(subscribe)という機能もあります。急にいっぱい出てきて混乱しますが、現時点では理解していなくて問題ありません。

Reduxの全体像を示します。

Reduxの全体像
Reduxが難しく感じるポイントは、この用語の多さだと思います。
1つずつ話を進めていきたいと思います。



ステート(state)

それでは早速、構成要素を1つずつ見ていきたいと思います。まずはステート(state)です。
ステート(state)は、アプリケーションで扱う全データを保持するオブジェクトです。大きな1つのJSONと考えるわかりやすいかもしれません。
ここでは、タイトル(title)とカウント(count)を保持するオブジェクトとしましょう。
// ステート
{
    title : '',
    count : 0
}
ただのJavaScriptオブジェクトですね!全く怖くありません(笑)。
このステートを、Reduxという考え方で管理したいと思います。



ストア(store)

上記のステートを管理する人(管理者)として、ストア(store)というものを定義します。Reduxの全体像のイメージで示した通り、ストア(store)がステート(state)を保持します(Store has State)。
// ストア
const store = {

    // ストアが、ステートを、保持している.
    state : {
        title : '',
        count : 0
    }
}
ストアの中に、ステートがあります。これもわかりやすいですね、簡単なJavaScriptです。
Reduxでは、ストアがステートを保持していて、ステートの変更はストアが行います。



ステートの値を変更する

それではステートの中身を変更したいと思います。ステートを変更する場合、アクション(action)、ディスパッチ(dispatch)、リデューサー(Reducer)の3つを使います。

ここでは「ステートの中のcountを1つ増やす」ことを考えてみたいと思います。


アクション(Action)

ストアに変更したい内容を伝えるため、アクション(action)を作成します。アクションは変更指示書のようなものです。
// アクション.
const action = {
    type : 'ADD_COUNT'
}
アクションも簡単なJavaScriptオブジェクトですね!
アクションにはtypeという決まった項目を用意し、その項目に指示を書きます。ここではADD_COUNTという文字列を指定しました。


ディスパッチ(dispatch)

指示書を作っても、それがストアに届かなければ意味がありません。ストアに届けるために、ディスパッチ(dispatch)を使います。
ストア(store)にdispatchというメソッドを用意し、引数でアクションを受け取ります。
const store = {

    // 上記のステートを、ストアは保持している.
    state : {
        title : '',
        count : 0
    },

    // ディスパッチを追加.
    dispatch : function (action) {
        // あとで実装します.
    }
}
これで、ストアにアクションを渡すことができます。


リデューサー(reducer)

ストアは受け取ったアクションを読み取って、ステートを変更します。アクションの内容を理解してステートを変更する処理を担当するのが、リデューサー(reducer)です。

ここではまず、リデューサー(reducer)の処理を定義したいと思います。
リデューサー(reducer)は、現在のステートとアクションを引数で受け取り、変更後の新しいステートを返却する関数として定義します。
// リデューサー(reducer).
// @param state 現在のステート
// @param action 変更内容
function myReducer(state, action) {

    // actionのタイプごとに、処理を分ける
    switch (action.type) {

        // ADD_COUNTの場合は、countを1増やす.
        case 'ADD_COUNT':
            state = {
                ...state,
                count : state.count + 1
            }
            return state

        default:
            return state
    }
}
リデューサーの中ではアクションのtype項目を見て、それぞれに応じた処理を行います。ここではtypeADD_COUNTの場合に、ステート内のcountに1加えています。

これで、アクション内容を理解してステートを変更する処理(=リデューサー)を作成できました。

なおReduxでは、既存のステートを変更するのではなく、新しいステートを毎回作成します。
// 新しいステートのオブジェクトを作成している.
state = {
    ...state,
    count : state.count + 1
}
これはReduxの3原則の一つ「State is read-only」でとても大切なことですが、最初のうちは気にしなくて良いと思います。まずは大枠を掴むことが大切です。

上記で作成したmyReducerを、storeに設定します。
// Store に reducer を追加.
const store = {

    state : {
        title : '',
        count : 0
    },

    // リデューサー.
    reducer : myReducer,

    dispatch : function (action) {
        // あとで実装します.
    }
}
これで、ストアがリデューサーを使えるようになりました。

そして、このreducerdispatchメソッドの中で使うことで、ステートを変更できるようになります。
// dispatch メソッド内で、reducer を使う.
const store = {

    state : {
        title : '',
        count : 0
    },

    reducer : myReducer,

    dispatch : function (action) {
        // reducer を使って、state を変更する.
        this.state = this.reducer(this.state, action)
    }
}
これで、ストア内のステートを変更できました。



実際に、Action → Dispatch → Reducer で State を変更してみる

実際に値を変更してみましょう。上記を実装した状態で、下記のコードを実行します。
// アクションを定義.
const action = {
    type : 'ADD_COUNT'
}

// 動作テスト
store.dispatch(action)
console.log('1回目:', store.state)  // 1回目: {title: "", count: 1}

store.dispatch(action)
console.log('2回目:', store.state)  // 2回目: {title: "", count: 2}

store.dispatch(action)
console.log('3回目:', store.state)  // 3回目: {title: "", count: 3}
無事に、アクション、ディスパッチ、リデューサー、を使って、ステートの値を変更できました。

実際にReduxのライブラリを使う場合には、store.stateとプロパティに直接アクセスするのではなく、store.getState()と関数を通してステートを取得します。
ここでもそれにならって、getStateメソッドを定義したいと思います。
// Store.
const store = {

    /* 省略 */

    // ステートを取得するメソッドを追加.
    getState : function () {
        return this.state
    }
}
上記のメソッドを使って、ステートを取得しましょう。
store.dispatch(action)
console.log('4回目:', store.getState())  // 4回目: {title: "", count: 4}
無事にステートを取得することができました。



変更を検知する(サブスクライブ(subscribe))

さて、ステートを変更するのが自分であればステート変更のタイミングは分かりますが、誰か他の人が(=他の処理が)更新した時には、ステートがいつ変更されたのかわかりません。他の人がステートを変更した際に、その変更を教えてもらうのが、サブスクライブ(subscribe)という機能です。

ストア内にsubscribeというメソッドを用意して、変更があった時に呼び出してもらう関数を登録できるようにしましょう。
// Store.
const store = {

    /* 省略  */

    // 変更時に呼び出す関数を保持する変数.
    subscribers : [],

    // 引数で受け取った関数を、subscribers変数に追加する.
    subscribe : function (fn) {
        this.subscribers.push(fn)
    }
}
そして、subscribersに登録された関数を、ストアのdispatchメソッドの中で呼び出し、ステート変更を通知します。
// Store.
const store = {

    /* 省略. */

    dispatch : function (action) {
        this.state = this.reducer(this.state, action)

        // ステートの変更後、変更を通知する.
        this.subscribers.forEach(function (subscriber) {
            subscriber()
        })
    },

    subscribers : [],

    subscribe : function (fn) {
        this.subscribers.push(fn)
    }
}
これで、サブスクライブ(subscribe)の実装ができました。

実際に使ってみたいと思います。
// ストアにサブスクライブを追加.
store.subscribe(function () {
    console.log('subscribe:', store.getState())
})

// ステートを変更する(1回目)
store.dispatch(action)     // subscribe: {title: "", count: 1}

// ステートを変更する(2回目)
store.dispatch(action)     // subscribe: {title: "", count: 2}
ステートに変更があった場合に、変更を検知することができました。



Reduxのメリット

さて最後に、Reduxを導入するメリットを考えてみたいと思います。こんなに大変なデータ管理をする必要があるのでしょうか。正直なところ、メリットを感じないうちは使う必要はありません。ただ、大きめなプロダクトを開発する時には、下記3つのメリットが生きてきます。

  1. データの変更方法が制限されている
  2. データの変更を検知する術がある
  3. 離れたコンポーネント間でデータのやり取りができる

「1. データの変更方法が制限されている」は一見不便そうですが、プロダクトが大きくなるとメリットがあります。データを変更する方法が限られるので、「このデータ、誰が変えたの?」という問題が発生しても、すぐに調べて原因を特定できます。データの更新方法が制限されていたり、データの更新者が制限されることで、アプリケーションのデータの堅牢性が上がります。

「2. データの変更を検知する術がある」は、Reduxの「サブスクライブ(subscribe)」という機能です。例えば、画面ヘッダーのコンポーネントで、「誰が更新したかはわからないけど、ステートの中のtitleが変わったら、自分の表示を更新しよう」といった実装ができるようになります。変更を通知してくれる仕組みは非常に便利です。

最後に「3. 離れたコンポーネント間でデータのやり取りができる」は、「2. データの変更を検知する術がある」とも関連しますが、例えばスレッドを表示する画面があったとします。「スレッドが切り替わるたびに、スレッド名を画面ヘッダーに反映したい。僕(=スレッド画面)はステートのtitleを更新すれば、君(=画面ヘッダー)がそれをサブスクライブしているから自動的に変更してくれるよね、ありがとう」といった使い方ができます。
特にReactなど、親子関係以外のコンポーネント間でデータをやり取りしたい場合に便利です。

他にもメリットがあると思いますが、Reduxを使うことで上記のメリットを享受できます。



参考サイト

Reduxの本家ページなど、リンクを掲載します。英語ですが、読むとためになります。

redux.js.org(本家ページ)

redux | Github(コードが読めます)



最後に

今日はReduxのチュートリアルを書きました。本記事で紹介したコードはどれも簡単なものなので、ぜひ実装してみてください。理解に役立ちます。

なお、Reduxの全体像を把握することを主眼としているため、実装内容の一部がReduxに準拠していない場合もあります。実際にReduxを使う場合は、Reduxのライブラリを使うと良いと思います。

最後になりますが本ブログでは、フロントエンド、PHP、Python、インフラ、サーバー、Swift、Node.js、Java、Linux、機械学習、などの技術トピックを発信をしていきます。「プログラミングで困ったその時の、解決の糸口に!」そんな目標でブログを書き続けています。ぜひ、本ブログのRSSTwitterをフォローして貰えたら嬉しいです ^ ^

最後までご覧頂きましてありがとうございました!





こんな記事もいかがですか?

[取り組み] フロントエンドでコーディングスピードをアップさせる6つの方法!と思って書いてたら30個も書いちゃった。
[フロントエンド] フロントエンドの入社試験99問!難しいですよ〜w。
[フロントエンド] Webページを表示するテストの際に、通信速度を3Gに制限して表示してみよう
[フロントエンド] スマホ実機でのデバッグ手段を増やす!Macのプロキシを利用して、通信内容を確認する。
[フロントエンド] Chrome 35 Beta の変更点。Touch制御、新しいJavaScript機能、プレフィックスなしのShadowDOM
[フロントエンド]複数アカウントでのテストには、Chromeのユーザー管理を使って、Cookieを切り替えると便利
[フロントエンド] Chrome36βが出た。変更点など。element.animate、HTML Imports、Object.observe、他。
RSS画像

もしご興味をお持ち頂けましたら、ぜひRSSへの登録をお願い致します。