LoginSignup
65
42

More than 3 years have passed since last update.

今から始めるReact入門 〜 Redux 編: Redux 単体で状態管理をしっかり理解する

Last updated at Posted at 2018-09-15

目次

注意事項

このページにて出てくるredux-promise ですが、ver6 から書き方が一律以下のように変更になりました。

import promise from "redux-promise-middleware";
/* ... */
const middleware = applyMiddleware(promise(), /* ... */);

/* ↓ ↓ ↓ ↓ ↓ */

import { createPromise } from 'redux-promise-middleware';
const promise = createPromise({ types: { fulfilled: 'success' } });
/* ... */
const middleware = applyMiddleware(promise, /* ... */);

GitHub リポジトリ側は既に修正してありますので、正確なソースコードについてはそちらの参照をお願いします。

Redux の動きを確認する

Redux の概要とimmutability の基礎を理解したら、次は簡単なReact + Redux のアプリケーションを作成する前にRedux 単体の動きを見てみましょう。
Redux は複雑に見えるかもしれませんが、ここではReact と切り離すことで、Redux がどのようにStore を管理するのかという部分に注目してみましょう。

Redux 実践

Redux プロジェクトの作成

Redux を単体で触るためのプロジェクトを作成します。
初期状態のファイルはGitHub 上にもあるので参考になればと思います。

Projectの初期化
$ mkdir react-redux-minimum
$ cd react-redux-minimum
$ npm init . -y
$ npm install --save-dev @babel/core @babel/preset-env @babel/preset-react babel-loader babel-plugin-react-html-attrs webpack webpack-cli webpack-dev-server
$ npm install --save-dev redux

package.json にwebpack-dev-server を起動するスクリプトを登録します。

package.json
  // ......
  "scripts": {
    "start": "webpack-dev-server --content-base src --mode development --inline",
    // ......
  },
  // ......
webpack.config.js
var debug   = process.env.NODE_ENV !== "production";
var webpack = require('webpack');
var path    = require('path');

module.exports = {
  context: path.join(__dirname, "src"),
  entry: "./js/client.js",
  module: {
    rules: [{
      test: /\.jsx?$/,
      exclude: /(node_modules|bower_components)/,
      use: [{
        loader: 'babel-loader',
        options: {
          presets: ['@babel/preset-react', '@babel/preset-env']
        }
      }]
    }]
  },
  output: {
    path: __dirname + "/src/",
    filename: "client.min.js",
    publicPath: '/'
  },
  devServer: {
    historyApiFallback: true
  },
  plugins: debug ? [] : [
    new webpack.optimize.UglifyJsPlugin({ mangle: false, sourcemap: false }),
  ],
};

プロジェクトを作成したら、src/index.html の作成と空のsrc/js/client.js ファイルを作成します。

src/index.html
<!DOCTYPE html>
<html>
  <head>
    <title>Redux Tutorials</title>
  </head>
  <body>
    <script src="client.min.js"></script>
  </body>
</html>

最終的に以下のような構成になります。

構成
react-redux-ex-simple
  + src/
    + js/
      + client.js
    + index.html
  + package.json
  + webpack.config.js

準備ができたら、npm start を実行してGoogle Chrome でhttp://localhost:8080 を開いて、開発者モードのコンソールを開いてください。
ここからはredux の動きをこのコンソールをつかって確認していきます。
redux_react0000.png

Redux のHello World

コンソールを開いたままの状態でsrc/js/client.js でredux をimport してみましょう。
Redux を使うには、まずredux コンポーネントをimport することです。次にReducer, Store を作成します。

src/js/client.js
import { createStore } from "redux";

const reducer = () => {
  console.log("reducer has been called.");
}

const store = createStore(reducer, 1);

createStore にはReducer とデータの初期値を渡します。
この段階ではReducer は初期値を設定するために呼ばれることになり、client.js を保存してコンソールを確認すると初期値を設定するためにReducer が呼ばれたことが確認できます。

redux_react0001.gif

一般的にデータの初期値はObject が使われますが、今回はシンプルにするため、プリミティブ型(int)を使うようにしています。
これからこの数値をインクリメントする処理を作成していきます。

dispatch メソッドの作成

次に、Store が変更された時に呼ばれるsubscribe メソッドとStore にAction を送信するdispatch メソッドを追加します。

src/js/client.js
 import { createStore } from "redux";

 const reducer = () => {
   console.log("reducer has been called.");
 }

 const store = createStore(reducer, 1);

+store.subscribe(() => {
+  console.log("store changed", store.getState());
+});
+
+store.dispatch({type: "INC"});

dispatch するところでも同様にReducer が呼ばれます。
コンソールを確認すると初期処理時とdispatch 時の2 回呼ばれ、その後にdispatch によりStore が変更されたことを検知してsubscribe メソッドが呼ばれます。
redux_react0002.gif

Action

次はReducer でAction を受け取って、Action のtype 毎に処理を分岐させてみます。

src/js/client.js
 import { createStore } from "redux";

-const reducer = () => {
-  console.log("reducer has been called.");
+const reducer = (state = 0, action) => {
+  switch(action.type) {
+    case "INC":
+      return state + 1;
+  }
+  return state;
 }

 const store = createStore(reducer, 1);

 store.subscribe(() => {
   console.log("store changed", store.getState());
 });

 store.dispatch({type: "INC"});

コンソールを確認すると、初期値がインクリメントされた結果が出力されます。
redux_react0003.gif

もっとインクリメントしてみましょう。

src/js/client.js
 import { createStore } from "redux";

 const reducer = (state = 0, action) => {
   switch(action.type) {
     case "INC":
       return state + 1;
   }
   return state;
 }

 const store = createStore(reducer, 1);

 store.subscribe(() => {
   console.log("store changed", store.getState());
 });

 store.dispatch({type: "INC"});
+store.dispatch({type: "INC"});
+store.dispatch({type: "INC"});
+store.dispatch({type: "INC"});

redux_react0004.gif

しっかりと1 つ前のstate の値を引き継いでインクリメントができています。
次はデクリメントを追加してみましょう。

src/js/client.js
 import { createStore } from "redux";

 const reducer = (state = 0, action) => {
   switch(action.type) {
     case "INC":
       return state + 1;
+    case "DEC":
+      return state - 1;
   }
   return state;
 }

 const store = createStore(reducer, 1);

 store.subscribe(() => {
   console.log("store changed", store.getState());
 });

 store.dispatch({type: "INC"});
 store.dispatch({type: "INC"});
 store.dispatch({type: "INC"});
 store.dispatch({type: "INC"});
+store.dispatch({type: "DEC"});

redux_react0004_02.gif

4 回インクリメントされた後、1 回デクリメントされて最後は4 になります。
このようにRedux は非常にシンプルで、Action に追加のデータを入れてあげればReducer でもそのデータを扱えるようになります。

次はAction をもう少し拡張してpayload を追加し、Reducer の中でpayload に指定した分だけインクリメント/デクリメントするような処理を作成してみましょう。
この場合、Reducer の中でpayload を読み込む必要がありますが、action.payloadのようにすれば読み込めます。

src/js/client.js
 import { createStore } from "redux";

 const reducer = (state = 0, action) => {
   switch(action.type) {
     case "INC":
-      return state + 1;
+      return state + action.payload;
     case "DEC":
-      return state - 1;
+      return state - action.payload;
   }
   return state;
 }

 const store = createStore(reducer, 1);

 store.subscribe(() => {
   console.log("store changed", store.getState());
 });

-store.dispatch({type: "INC"});
-store.dispatch({type: "INC"});
-store.dispatch({type: "INC"});
-store.dispatch({type: "INC"});
-store.dispatch({type: "DEC"});
+store.dispatch({type: "INC", payload: 1});
+store.dispatch({type: "INC", payload: 2});
+store.dispatch({type: "INC", payload: 22});
+store.dispatch({type: "INC", payload: 222});
+store.dispatch({type: "DEC", payload: 1000});

redux_react0005.gif

しっかりとpayload の値でインクリメント/デクリメントができています。
当然ですがこのpayload はオブジェクトや配列も指定することができるので、状況に合ったデータの形で指定するようにしてください。

Redux のいったんまとめ

Redux の基本的な流れをまとめると...
* reducer を作成する
* store を作成する
* action をdispatch する
* 単一のreducer もしくは複数のreducer が単一のstore に対して処理を行う
となります。
次のサンプルでは複数のreducer を1 つにまとめるやり方について実施していきます。

複数のReducer

Redux では複数のreducer を準備し、そのreducer を統合することもできます。
tweets をするアプリケーションをつくるつもりでsrc/js/client.js を一度以下のように作成し直してみましょう。

src/js/client.js
import { combineReducers, createStore } from "redux";

const userReducer = (state, action) => {}

const tweetsReducer = (state, action) => {}

const store = createStore(reducers, { user: { name: "Tsutomu", age: 35 }, twiits: [] });

store.subscribe(() => {
  console.log("store changed", store.getState());
});

上記のようにuserReducertweetsReducer を作成します(本来userReducer, tweetsReducer は別ファイルに外出しすべきメソッドです)。
また、上記のReducer を結合するためにcombinedReducers もロードします。
下記はcombinedReducers を使って2 つのreducer を結合しています。

src/js/client.js
 import { combineReducers, createStore } from "redux";

 const userReducer = (state, action) => {}

 const tweetsReducer = (state, action) => {}

+const reducers = combineReducers({
+  user: userReducer,
+  tweets: tweetsReducer
+});
+
 const store = createStore(reducers, { user: { name: "Tsutomu", age: 35 }, twiits: [] });

 store.subscribe(() => {
   console.log("store changed", store.getState());
 });

combineReducers には変更しようとしているState データを記述します。
今回の場合はname とage を持つuser Object とツイート内容を格納するtweets 配列でそれらはuserReducer, tweetsReducer によって変更されるものです。

次はデータの初期値をcreateStore 内ではなく、各reducer の引数にES6 書式でデフォルト値を指定してみましょう。
また、userReducer, tweetsReducer はそれぞれデフォルト値をreturn するように以下のように書き換え、store.subscribe メソッドでデバッグプリントを出力するために、適当にdispatch を飛ばしてみます。

src/js/client.js
 import { combineReducers, createStore } from "redux";

-const userReducer = (state, action) => {}
+const userReducer = (state = {}, action) => {
+  return state;
+}

-const tweetsReducer = (state, action) => {}
+const tweetsReducer = (state = [], action) => {
+  return state;
+}

 const reducers = combineReducers({
   user: userReducer,
   tweets: tweetsReducer
 });

-const store = createStore(reducer, { user: { name: "Tsutomu", age: 35 }, twiits: [] });
+const store = createStore(reducers);

 store.subscribe(() => {
   console.log("store changed", store.getState());
 });
+
+store.dispatch({type: "FOO", payload: "BAR"});

プログラムを書き終えたら、一旦ブラウザのコンソールを確認してみましょう。
次のようにuser とtweets にそれぞれ初期値としてからのObject と空の配列が作成されていれば大枠は完了です。
redux_react0006.gif

次にdispatch とreducer の処理を書いていきましょう。
以下のように名前を変更するAction タイプCHANGE_NAME と年齢を変更するAction タイプCHANGE_AGE を作成します。

src/js/client.js
 import { combineReducers, createStore } from "redux";

 const userReducer = (state = {}, action) => {
+  switch(action.type) {
+    case "CHANGE_NAME":
+      state.name = action.payload;
+      break;
+    case "CHANGE_AGE":
+      state.age = action.payload;
+      break;
+  }
   return state;
 }

 const tweetsReducer = (state = [], action) => {
   return state;
 }

 const reducers = combineReducers({
   user: userReducer,
   tweets: tweetsReducer
 });

 const store = createStore(reducers);

 store.subscribe(() => {
   console.log("store changed", store.getState());
 });

-store.dispatch({type: "FOO", payload: "BAR"});
+store.dispatch({type: "CHANGE_NAME", payload: "Tsutomu"});
+store.dispatch({type: "CHANGE_AGE", payload: 35});

ここでWeb ブラウザのコンソール出力を確認してみましょう。
redux_react0007.gif

するとどうでしょう。最終的な結果としてuser: {name: "Tsutomu", age: 35} が設定されているので正常に動作しているように見えるのですが、1 回目の結果も確認してみましょう。
1 回目は、この時点ではまだuser: {name: "Tsutomu"} しか設定していないはずなのにuser: {name: "Tsutomu", age: 35} と結果が出力されてしまっています。

これは1 回目のuserReducer でreturn しているオブジェクトと2 回目のuserReducer でreturn しているオブジェクトが完全に同一のオブジェクトであるため、かつJavaScript の非同期性の特性からstore.subscribe 内にあるconsole.log が呼ばれる時点で、user とage が既に設定されてしまっている、完全に同じオブジェクトを出力してしまうわけです。
そのため、1 回目のuserReducer と2 回目のuserReducer で完全に異なるオブジェクトを返してあげるようにすればこの問題は解決します。
ではどのようにすれば、1 回目と2 回目で異なるオブジェクトを返せるようになるかと言うと、先程説明したimmutable なJavaScript が答えになってきます。
Object に値を設定する時にObject.assign もしくはES6 の記法を使うのであれば{...state, name: action.payload}のような記法を使ってやれば良いのです。
では実際にその方法で書き換えて、もう一度結果を確認してみましょう。

src/js/client.js
 import { combineReducers, createStore } from "redux";

 const userReducer = (state = {}, action) => {
   switch(action.type) {
     case "CHANGE_NAME":
-      state.name = action.payload;
+      state = {...state, name: action.payload}
       break;
     case "CHANGE_AGE":
-      state.age = action.payload;
+      state = {...state, age: action.payload}
       break;
   }
   return state;
 }

 const tweetsReducer = (state = [], action) => {
   return state;
 }

 const reducers = combineReducers({
   user: userReducer,
   tweets: tweetsReducer
 });

 const store = createStore(reducers);

 store.subscribe(() => {
   console.log("store changed", store.getState());
 });

 store.dispatch({type: "CHANGE_NAME", payload: "Tsutomu"});
 store.dispatch({type: "CHANGE_AGE", payload: 35});

redux_react0008.gif
想定通りの動きになりました。次は、age を35 に設定した後にage を36 に設定して見ましょう。

src/js/client.js
 import { combineReducers, createStore } from "redux";

 const userReducer = (state = {}, action) => {
   switch(action.type) {
     case "CHANGE_NAME":
       state = {...state, name: action.payload}
       break;
     case "CHANGE_AGE":
       state = {...state, age: action.payload}
       break;
   }
   return state;
 }

 const tweetsReducer = (state = [], action) => {
   return state;
 }

 const reducers = combineReducers({
   user: userReducer,
   tweets: tweetsReducer
 });

 const store = createStore(reducers);

 store.subscribe(() => {
   console.log("store changed", store.getState());
 });

 store.dispatch({type: "CHANGE_NAME", payload: "Tsutomu"});
 store.dispatch({type: "CHANGE_AGE", payload: 35});
+store.dispatch({type: "CHANGE_AGE", payload: 36});

redux_react0009.gif

こちらの動きも問題ありません。

次はtweetsReducer の方にも処理を実装してみましょう。
userReducer, tweetsReducer の2 つはdispatch が行われるたびに、シーケンシャルに呼ばれるようになります。
今までもstore.dispatch が呼ばれるたびにuserReducer, tweetsReducer がシーケンシャルに呼ばれていました。
userReducer にはaction タイプCHANGE_NAME, CHANGE_AGE に対する処理を実装していて、tweetsReducer 内でもそれらのaction に対しての処理を書くことができますが、処理が追いづらくなり、副作用が生まれやすいのであまり推奨はしません。
userReducer, tweetsReducer それぞれで異なったAction タイプを処理するように実装するのがシンプルでわかりやすくなります。

ということでtweetsReducer にはADD_TWEET Action タイプを処理するように実装をしてみましょう。

src/js/client.js
 import { combineReducers, createStore } from "redux";

 const userReducer = (state = {}, action) => {
   switch(action.type) {
     case "CHANGE_NAME":
       state = {...state, name: action.payload}
       break;
     case "CHANGE_AGE":
       state = {...state, age: action.payload}
       break;
   }
   return state;
 }

 const tweetsReducer = (state = [], action) => {
+  switch(action.type) {
+    case "ADD_TWEET":
+      state = state.concat({id: Date.now(), text: action.payload});
+  }
   return state;
 }

 const reducers = combineReducers({
   user: userReducer,
   tweets: tweetsReducer
 });

 const store = createStore(reducers);

 store.subscribe(() => {
   console.log("store changed", store.getState());
 });

 store.dispatch({type: "CHANGE_NAME", payload: "Tsutomu"});
 store.dispatch({type: "CHANGE_AGE", payload: 35});
 store.dispatch({type: "CHANGE_AGE", payload: 36});
+store.dispatch({type: "ADD_TWEET", payload: "OMG LIKE LOL"});
+store.dispatch({type: "ADD_TWEET", payload: "I am so like seriously like totally like right now"});

redux_react0010.gif

ADD_TWEET を実装することができました。Redux にて複数reducer を利用するイメージがこれでついたことと思います。
ID は重複してしまっていますが、本格的なアプリ作成時にはそのあたりも考慮するようにしてください。

middleware

ここではredux とmiddleware を組み合わせて利用する例について説明していきます。
Redux を使っているとreducer を呼ぶ前に処理を幾つか追加したいことがあります。
たとえばREST API で動いているバックエンドから、reducer で処理するためのJSON データを取得してきたり、reducer の処理に入る前後にログを出力したりといった処理です。
そのような要望がある場合はRedux のmiddleware を使いましょう。

src/js/client.js をもう一度作り直し、以下のようにします。

src/js/client.js
import { createStore } from "redux";

const reducer = (state = 0, action) => {
  switch(action.type) {
    case "INC":
      state = state + 1;
      break;
    case "DEC":
      state = state - 1;
      break;
  }
  return state;
}

const store = createStore(reducer, 1);

store.dispatch({type: "INC"});
store.dispatch({type: "INC"});
store.dispatch({type: "DEC"});
store.dispatch({type: "DEC"});

applyMiddleware をimport し、applyMiddleware 関数を使ってmiddleware を定義します。
(ここではまだ渡していませんが)applyMiddleware 関数の引数に関数を渡すと、引数として渡された関数がmiddleware としてRedux に認識されるようになります。

src/js/client.js
-import { createStore } from "redux";
+import { applyMiddleware, createStore } from "redux";

 const reducer = (state = 0, action) => {
   switch(action.type) {
     case "INC":
       state = state + 1;
       break;
     case "DEC":
       state = state - 1;
       break;
   }
   return state;
 }
+
+const middleware = applyMiddleware();

 const store = createStore(reducer, 1);

 store.dispatch({type: "INC"});
 store.dispatch({type: "INC"});
 store.dispatch({type: "DEC"});
 store.dispatch({type: "DEC"});

今回は練習としてlogger 関数を作成し、それをmiddleware として使っていこうと思います。
logger 関数はAction 内容を出力する関数で、logger 関数を以下のように作成しapplyMiddleware 関数へ渡します。

src/js/client.js
 import { applyMiddleware, createStore } from "redux";

 const reducer = (state = 0, action) => {
   switch(action.type) {
     case "INC":
       state = state + 1;
       break;
     case "DEC":
       state = state - 1;
       break;
   }
   return state;
 }

-const middleware = applyMiddleware();
+const logger = (store) => (next) => (action) => {
+  console.log("action fired", action);
+}
+
+const middleware = applyMiddleware(logger);

 const store = createStore(reducer, 1);
補足
const logger = (store) => (next) => (action) => {
  console.log("action fired", action);
}

// ↓ ↓ ↓

function logger(store) {
  return function (next) {    /* 無名関数 */
    return function (action) {    /* 無名関数 */
      console.log("action fired", action);
    }
  }
}

middleware を作成したらcreateStore の第3 引数に渡してstore を拡張します。
そうすることによってaction がdispatch された時にmiddleware も実行されるようになります(?)。

src/js/client.js
 import { applyMiddleware, createStore } from "redux";

 const reducer = (state = 0, action) => {
   switch(action.type) {
     case "INC":
       state = state + 1;
       break;
     case "DEC":
       state = state - 1;
       break;
   }
   return state;
 }

 const logger = (store) => (next) => (action) => {
   console.log("action fired", action);
 }

 const middleware = applyMiddleware(logger);

-const store = createStore(reducer, 1);
+const store = createStore(reducer, 1, middleware);

 store.dispatch({type: "INC"});
 store.dispatch({type: "INC"});
 store.dispatch({type: "DEC"});
 store.dispatch({type: "DEC"});

ここでWeb ブラウザのconsole を確認してみましょう。
redux_react0011.gif

するとmiddleware として登録したlogger 関数がdispatch されるタイミングで呼ばれていることがわかります。
次に前のサンプルでも使っていたsubscribe メソッドを入れてみましょう。

src/js/client.js
 import { applyMiddleware, createStore } from "redux";

 const reducer = (state = 0, action) => {
   switch(action.type) {
     case "INC":
       state = state + 1;
       break;
     case "DEC":
       state = state - 1;
       break;
   }
   return state;
 }

 const logger = (store) => (next) => (action) => {
   console.log("action fired", action);
 }

 const middleware = applyMiddleware(logger);

 const store = createStore(reducer, 1, middleware);

+store.subscribe(() => {
+  console.log("store changed", store.getState());
+});
+
 store.dispatch({type: "INC"});
 store.dispatch({type: "INC"});
 store.dispatch({type: "DEC"});
 store.dispatch({type: "DEC"});

この時点でコンソールを確認しても先程の出力と変わりは無く、console.log("store changed", store.getState()); の出力はまだ出ません。
その理由はこのソースコードではまだstore を変更する処理は入っていないからです。
また、dispatch された後、これまでのサンプルではreducer が呼ばれていましたがmiddleware を使う場合はmiddleware (今回の例ではlogger 関数)が呼ばれた後にreducer が呼ばれるように処理の最後にnext(action); を追加します。

next()が呼ばれた時の処理の流れ
+--------------+               +--------------+
| middleware01 | -- next() --> | reducer      |
+--------------+               +--------------+
src/js/client.js
 import { applyMiddleware, createStore } from "redux";

 const reducer = (state = 0, action) => {
   switch(action.type) {
     case "INC":
       state = state + 1;
       break;
     case "DEC":
       state = state - 1;
       break;
   }
   return state;
 }

 const logger = (store) => (next) => (action) => {
   console.log("action fired", action);
+  next(action);
 }

 const middleware = applyMiddleware(logger);

 const store = createStore(reducer, 1, middleware);

 store.subscribe(() => {
   console.log("store changed", store.getState());
 });

 store.dispatch({type: "INC"});
 store.dispatch({type: "INC"});
 store.dispatch({type: "DEC"});
 store.dispatch({type: "DEC"});

ここでコンソールを確認してみましょう。
redux_react0012.gif

reducer も呼ばれるようになりました。
上記出力結果を見てわかるようにdispatch が行われるとmiddleware -> reducer という順番で処理が進むことになり、reducer で処理するAction もmiddleware は見れるようになっています。
そのため、例えば以下のようにmiddleware 内で常にaction.type を"DEC" になるよう処理を書いてしまうと…

src/js/client.js
 import { applyMiddleware, createStore } from "redux";

 const reducer = (state = 0, action) => {
   switch(action.type) {
     case "INC":
       state = state + 1;
       break;
     case "DEC":
       state = state - 1;
       break;
   }
   return state;
 }

 const logger = (store) => (next) => (action) => {
   console.log("action fired", action);
+  action.type = "DEC";
   next(action);
 }

 const middleware = applyMiddleware(logger);

 const store = createStore(reducer, 1, middleware);

 store.subscribe(() => {
   console.log("store changed", store.getState());
 });

 store.dispatch({type: "INC"});
 store.dispatch({type: "INC"});
 store.dispatch({type: "DEC"});
 store.dispatch({type: "DEC"});

結果は常にaction.type が"DEC" の処理を実行した時の結果となります。
redux_react0013.gif

このようにmiddleware がある場合はmiddleware の最後の方にnext(action); をつけるように意識する一方で、middleware 内で実施した変更がreducer に対して副作用を持たせないよう注意してください。

複数のmiddleware

次にmiddleware を複数登録する場合についてのやり方です。
applyMiddleware では複数のmiddleware を登録できるようになっています。
複数のmiddleware を登録した場合、第1 引数に指定した関数を1 番最初に実行し、その中でnext() を呼び出していれば第2 引数以降の関数を順番に実行していくという挙動をするようになります。
そして最後の関数でnext() が呼ばれると、処理はreducer に渡されます。

next()が呼ばれた時の処理の流れ(複数middleware)
+--------------+               +--------------+                       +--------------+               +--------------+
| middleware01 | -- next() --> | middleware02 | -- next() --> ... --> | middlewareN  | -- next() --> | reducer      |
+--------------+               +--------------+                       +--------------+               +--------------+

具体的にイメージしやすいように、次のようなサンプルを作成してみましょう。
error という関数を作成し、その中でtry catch 文を埋め込み、reducer でthrow された例外をハンドリングする処理を書いてみましょう。

src/js/client.js
 import { applyMiddleware, createStore } from "redux";

 const reducer = (state = 0, action) => {
   switch(action.type) {
     case "INC":
       state = state + 1;
       break;
     case "DEC":
       state = state - 1;
       break;
+    case "ERR":
+      throw new Error("It's error!!!!");
   }
   return state;
 }

 const logger = (store) => (next) => (action) => {
   console.log("action fired", action);
-  action.type = "DEC";
   next(action);
 }

-const middleware = applyMiddleware(logger);
+const error = (store) => (next) => (action) => {
+  try{
+    next(action);
+  } catch (e) {
+    console.log("Error was occured", e);
+  }
+}
+
+const middleware = applyMiddleware(logger, error);

 const store = createStore(reducer, 1, middleware);

 store.subscribe(() => {
   console.log("store changed", store.getState());
 });

 store.dispatch({type: "INC"});
 store.dispatch({type: "INC"});
 store.dispatch({type: "DEC"});
 store.dispatch({type: "DEC"});
+store.dispatch({type: "ERR"});

redux_react0014.gif

上記のコンソール出力内容はerror(middleware) 関数内で例外がcatch され出力されたものです。
このように複数middleware を使うことでmiddleware で発生した例外をハンドリングするためのmiddleware を定義することもできるのです。

非同期アプリケーション

middleware に非同期処理を入れることもできます。
非同期処理を体験するためにsrc/js/client.js を以下のように新しく書き換えます。

src/js/client.js
import { applyMiddleware, createStore } from "redux";

const reducer = (state={}, action) => {
  return state;
};

const middleware = applyMiddleware();
const store = createStore(reducer, middleware);

store.dispatch({type: "FOO"});

次にredux-logger をimport します。
redux-logger がインストールされていない場合はインストールしてください。

terminal
$ npm install --save-dev redux-logger
src/js/client.js
 import { applyMiddleware, createStore } from "redux";
+import { createLogger } from "redux-logger";

 const reducer = (state={}, action) => {
   return state;
 };

-const middleware = applyMiddleware();
+const middleware = applyMiddleware(createLogger());
 const store = createStore(reducer, middleware);

 store.dispatch({type: "FOO"});

ここでconsole を確認してみましょう。
するとredux-logger によって、action が起動した時に変更前のstate と変更後のstate が表示されます。

ReduxAsync_React0001.png

次にdispatcher を改修していきます。
前回までdispatcher はデータをdispatch していましたが、今回は関数をdispatch します。
関数をdispatch することで、先の処理でこの関数が実行されるような作りにしていきます。
dispatcher で渡す関数の中にdispatch 関数の実体を含め、非同期処理を組み込めるように変更します。

src/js/client.js
 import { applyMiddleware, createStore } from "redux";
 import { createLogger } from "redux-logger";

 const reducer = (state={}, action) => {
   return state;
 };

 const middleware = applyMiddleware(createLogger());
 const store = createStore(reducer, middleware);

-store.dispatch({type: "FOO"});
+store.dispatch((dispatch) => {
+  dispatch({type: "FOO"});
+  // do something async
+  dispatch({type: "BAR"});
+});

console を確認してみましょう。
すると、以下のようなエラーが出ます。
ReduxAsync_React0002.png

dispatcher は単純なObject が渡されることを期待していて、関数が渡されることを期待していないため出るエラーです。

redux-thunk を使う

これを解消するために"redux-thunk" を使用します。
redux-thunk はRedux のmiddleware で、Action オブジェクトの代わりに関数を返す処理を呼び出すことができるようにするためのミドルウェアです。
thunk はstore のdispatch メソッドを受け取り、Action オブジェクトの代わりに渡された非同期関数処理が完了した後に通常の同期処理アクションをディスパッチするために利用されます。

それではredux-thunk を取り入れて非同期処理を実装してみましょう。

terminal
$ npm install --save-prod redux-thunk
src/js/client.js
 import { applyMiddleware, createStore } from "redux";
 import { createLogger } from "redux-logger";
+import thunk from "redux-thunk";

 const reducer = (state={}, action) => {
   return state;
 };

-const middleware = applyMiddleware(createLogger());
+const middleware = applyMiddleware(thunk, createLogger());
 const store = createStore(reducer, middleware);

 store.dispatch((dispatch) => {
   dispatch({type: "FOO"});
   // do something async
   dispatch({type: "BAR"});
 });

ここでもう一度console を確認してみましょう。
redux_react0015.gif

今度はエラー無くdispatch に関数を渡すことができました。
では次は実際に非同期処理を埋め込んでみましょう。
今回はHTTP client として動作するaxios を利用して非同期なHTTP リクエストをさばいてみましょう。

terminal
$ npm install --save-dev axios
src/js/client.js
 import { applyMiddleware, createStore } from "redux";
+import axios from "axios";
 import { createLogger } from "redux-logger";
 import thunk from "redux-thunk";

 const reducer = (state={}, action) => {
   return state;
 };

 const middleware = applyMiddleware(thunk, createLogger());
 const store = createStore(reducer, middleware);

 store.dispatch((dispatch) => {
-  dispatch({type: "FOO"});
-  // do something async
-  dispatch({type: "BAR"});
+  dispatch({type: "FETCH_USERS_START"});
+  axios.get("http://localhost:18080").then((response) => {
+    dispatch({type: "RECEIVE_USERS", payload: response.data});
+  }).catch((err) => {
+    dispatch({type: "FETCH_USERS_ERROR", payload: err});
+  });
 });

更にreducer にも各Action タイプ毎の処理を追加していきましょう。

src/js/client.js
 import { applyMiddleware, createStore } from "redux";
 import axios from "axios";
 import { createLogger } from "redux-logger";
 import thunk from "redux-thunk";

 const reducer = (state={}, action) => {
+  switch (action.type) {
+    case "FETCH_USERS_START":
+      break;
+    case "FETCH_USERS_ERROR":
+      break;
+    case "RECEIVE_USERS":
+      break;
+  }
   return state;
 };

 const middleware = applyMiddleware(thunk, createLogger());
 const store = createStore(reducer, middleware);

 store.dispatch((dispatch) => {
   dispatch({type: "FETCH_USERS_START"});
   axios.get("http://localhost:18080").then((response) => {
     dispatch({type: "RECEIVE_USERS", payload: response.data});
   }).catch((err) => {
     dispatch({type: "FETCH_USERS_ERROR", payload: err});
   });
 });

ここで一旦console で動作確認をしようと思いますが、その前にterminal で以下のコマンドを実行し、ダミーサーバを起動しておきます。
GET リクエストを受け取ると1 秒後にレスポンスを返すダミーサーバです。

ダミーサーバ
$ node << EOF
var http = require('http');
http.createServer(function (req, res) {
  res.writeHead(200, {'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*'});
  setTimeout(() => res.end('{age: 30, id: 0, name: "foo", age: 25, id: 1, name: "bar"}'), 1000);
}).listen(18080);
EOF

準備ができたらconsole を確認してみましょう。
redux_react0016.gif

すると、FETCH_USERS_START Action が発行されてから1 秒後にRECEIVE_USERS が発行されるのがデバッグプリントから確認できます。

次は、state の初期値の定義とreducer 内でstate を書き換える処理を更に追記していきましょう。

src/js/client.js
 import { applyMiddleware, createStore } from "redux";
 import axios from "axios";
 import { createLogger } from "redux-logger";
 import thunk from "redux-thunk";

-const reducer = (state={}, action) => {
+const initialState = {
+  fetching: false,
+  fetched: false,
+  users: [],
+  error: null
+};
+
+const reducer = (state=initialState, action) => {
   switch (action.type) {
     case "FETCH_USERS_START":
-      break;
+      return {...state, fetching: true};
     case "FETCH_USERS_ERROR":
-      break;
+      return {...state, fetching :false, error: action.payload};
     case "RECEIVE_USERS":
-      break;
+      return {
+        ...state,
+        fetching: false,
+        fetched: true,
+        users: action.payload
+      };
   }
   return state;
 };

 const middleware = applyMiddleware(thunk, createLogger());
 const store = createStore(reducer, middleware);

 store.dispatch((dispatch) => {
   dispatch({type: "FETCH_USERS_START"});
   axios.get("http://localhost:18080").then((response) => {
     dispatch({type: "RECEIVE_USERS", payload: response.data});
   }).catch((err) => {
     dispatch({type: "FETCH_USERS_ERROR", payload: err});
   });
 });

redux_react0017.gif

state がinitialState -> fetching: true -> fetching: false, fetched: true, users: "{age: 30, id: 0, name...}" と各アクション毎に変更されているのが確認できます。
このようにして、実際のアプリケーション開発では時間のかかる非同期処理に対して"FETCH_USER_START" (処理中) と"RECEIVE_USERS" (処理完了) の2 つのAction を用いることにより、処理完了までプログレスバーやspinner を表示させるといった機能も実装することができるようになります。

redux-promise を使ってみる

これでRedux においての非同期処理の実装についてはほぼ完了ですが、その他の非同期処理を行うmiddleware としてredux-promise があります。
redux-promise を使用した場合の実装例は以下のようになります。

terminal
$ npm install --save-dev redux-promise-middleware
src/js/client.js
 import { applyMiddleware, createStore } from "redux";
 import axios from "axios";
 import { createLogger } from "redux-logger";
-import thunk from "redux-thunk";
+import promise from "redux-promise-middleware";

 const initialState = {
   fetching: false,
   fetched: false,
   users: [],
   error: null
 };

 const reducer = (state=initialState, action) => {
   switch (action.type) {
-    case "FETCH_USERS_START":
+    case "FETCH_USERS_PENDING":
       return {...state, fetching: true};
-    case "FETCH_USERS_ERROR":
+    case "FETCH_USERS_REJECTED":
       return {...state, fetching :false, error: action.payload};
-    case "RECEIVE_USERS":
+    case "FETCH_USERS_FULFILLED":
       return {
         ...state,
         fetching: false,
         fetched: true,
         users: action.payload
       };
   }
   return state;
 };

-const middleware = applyMiddleware(thunk, createLogger());
+const middleware = applyMiddleware(promise, createLogger());
 const store = createStore(reducer, middleware);

-store.dispatch((dispatch) => {
-  dispatch({type: "FETCH_USERS_START"});
-  axios.get("http://localhost:18080").then((response) => {
-    dispatch({type: "RECEIVE_USERS", payload: response.data});
-  }).catch((err) => {
-    dispatch({type: "FETCH_USERS_ERROR", payload: err});
-  });
+store.dispatch({
+  type: "FETCH_USERS",
+  payload: axios.get("http://localhost:18080")
 });

redux-promise-middleware を使うことでstore.dispatch の書き方がだいぶクリーンになりました。
redux-promise-middlewarestore.dispatch で指定したaction type をprefix にして*_PENDING(非同期処理未完了状態), *_ERROR(非同期処理エラー), *_FULFILLED(非同期処理正常終了) といったsuffix を追加してaction type を発行してくれます。
なので、reducer ではそれに合わせてaction type を定義して上げることでクリーンなコードを維持したまま、非同期処理の状態を簡単に管理できるようになります。

redux-saga を使う(概要のみ)

redux-thunk はReduxJS 組織によって開発されていますが、それとは別に近頃話題になってきているredux-saga を使って非同期処理を実装する方法もあります。
詳細は割愛しますが、redux-saga をつかった場合、次のようになります。

src/js/client.js
import { put, call, fork, takeEvery, all } from 'redux-saga/effects'
import { applyMiddleware, createStore } from "redux";
import axios from "axios";
import { createLogger } from "redux-logger";
import createSagaMiddleware from "redux-saga";

const initialState = {
  fetching: false,
  fetched: false,
  users: [],
  error: null
};

const fetchUser = () => axios.get('http://localhost:18080');

function* watchFetchUser() {
  yield takeEvery("FETCH_USERS", helloSaga);
}

function* rootSaga() {
  yield fork(watchFetchUser);
  //yield all([watchFetchUser(), ...]);
}

function* helloSaga() {
  yield put({type: "FETCH_USERS_START"});

  try {
    const response = yield call(fetchUser);
    yield put({type: "RECEIVE_USERS", payload: response});
  } catch (e) {
    yield put({type: "FETCH_USERS_ERROR", payload: e});
  }
}

const reducer = (state=initialState, action) => {
  switch (action.type) {
    case "FETCH_USERS_START":
      return {...state, fetching: true};
    case "FETCH_USERS_ERROR":
      return {...state, fetching :false, error: action.payload};
    case "RECEIVE_USERS":
      return {
        ...state,
        fetching: false,
        fetched: true,
        users: action.payload
      };
  }
  return state;
};

const sagaMiddleware = createSagaMiddleware();
const middleware = applyMiddleware(sagaMiddleware, createLogger());
const store = createStore(reducer, middleware);
sagaMiddleware.run(rootSaga);

store.dispatch({type: "FETCH_USERS"});

参考

65
42
3

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
65
42