ReduxのExampleを徹底図解

LINEで送る
Pocket

2010年代に入ってから、フロントエンドのJavascriptライブラリの群雄割拠でキャッチアップが大変ですが、どれも概念と設計が工夫されていて使っていて楽しいです。
jQueryのDOM操作はなんでもできるけど、やっているうちにDOMをちょこちょこ変更したり、何かの値を無理やりDOMのdata属性にもたせたり、コールバックをあちこちに書いたりして訳が分からなくなってきたりしますからね。

Backbone.jsが出てきてMVCを実現させてみて世間をあっと言わせたかと思えば、Angular.jsがこれだけで大抵のことはなんでもできるフルスタックなフレームワークとして登場してさらに便利になったり。
他にもKnockout.jsとかvue.jsとかいろいろありました。

Reactが登場してから、流れがReactに傾いている(使用する案件が増えている)気がします。
ReactはUIのためのフレームワークなので、データ(Reactではstateと呼ぶ)のフローを扱う仕組みが必要で、そのためのフレームワークはRedux一択になりつつあります。

Reduxの解説でよくでてくるのが、André Staltzさんの書いたこの図
redux

図の通りの動きをするのですが、初めてみたときはMiddlewareとかreducerとかよく分かりません。
なんとなく分かるのはViewからactionsをstoreに渡して、storeでごにょごにょして、その結果をまたViewに反映させるのだということですね。

大事なのはデータの流れが一方向だということです。
このメリットはいろいろなところで解説されていますが、要はデータの流れをわかりやすくしようということです。
そのための工夫がactionというViewから渡されるデータであったり、actionを処理してstateに反映させるreducerだったりするわけです。

reduxの公式Exampleはいろいろなパターンが用意されていて、眺めているとReduxというのものが理解できるようになっています。
ソースだけ読んでいると追っかけるのが大変なので、André Staltzさんの図にこのExampleをのっけてみましょう。

1.Counter Vanilla

Counter Vanilla

これはReactを使用していません。
renderというfunctionがリアルなDOMを描画(stateの値を表示)します。
getElementByIdやinnerHTML、addEventListenerというおなじみの関数でDOM操作をしているだけです。
ReduxはReactを推奨していますが、View Providerは自作でもAngurar.jsでも何でもいいのですね。

ボタンのクリックイベントに登録したfunction(無名関数)がactionオブジェクトを生成して、dispatchでstoreに渡して、reducerがactionを処理してstateに反映(Incrementというactionだったらstateに+1する等)させます。
stateに変更があったら、storeに登録(subscribe)しておいたrender関数がリアルなDOMを再描画するというわけです。
わかりやすいですね。

ちなみにIT業界ではvanillaは「ごく平凡な」という意味で使われます。
バニラアイスクリームが「ごく平凡な」アイスクリームだからだそうです。

2.Counter

Counter

Counter VanillaをReactにしただけです。
storeがrender関数を実行したら、ReactDOMのrenderが呼ばれて、stateに応じたCounter Component(仮想DOM)が再描画されます。

Counterという仮想DOMをまるごとつくりなおしていますが、実際にはリアルなDOMとの差分だけ再描画(Counter Vanillaのrenderと実質的には同じ)します。
Viewのどの部分を変えるのかを気にせず(仮想DOMの)全体を書き換えるので管理が楽にもかかわらず、描画効率がいいのがReactの特徴です。

3.Todos

Todos

いきなり複雑になりました。

react-reduxというライブラリを使用しています。
これがReact+Reduxのベースになるでしょう。

react-reduxではProviderというReact Componentで、storeとその他の React Componentを一緒にいれてしまいます。
React ComponentをPresentational ComponentとContainer Componentという二つの概念で区別します。
ソースファイルはそれぞれcomponentsとcontainersというディレクトリに区別されて置かれたりします。

Presentational ComponentはUIを定義する通常のReact Componentですが、Container Componentはreduxからstateの値を受け取るComponentです。
react-reduxのconnectという関数をつかって、stateをComponentのpropsに変換します。
Reactでは親Componentからpropsを受け取ってComponent内で仮想DOMを構築しますが、connect関数を使えば親子関係に関係なく任意のComponentでstateの値を受け取ることができます。
誰かがこの仕組みを「どこでもドア」と表現していました(笑)

4.Todos with Undo

Todos with Undo

上記のTodosにUndo機能がつきました。
これはredux-undoというライブラリを使用しています。

undo、redoというActionCreatorsの関数とundoableTodosというreducer関数内で読んでいるundoableという関数はredux-undoが提供するものです。
undoableは任意のactionを受け取って、そのときのstateを履歴にもっておきます。
undo、redoが生成するactionを受け取って、stateを履歴から呼び出してくれます。

5.TodoMVC

TodoMVC

TodoMVC(さまざまなJavascriptフレームワークによるTodoアプリ)のReact実装版です。
Container ComponentはAppだけ、reducerはtodosだけと、シンプルな構成になりましたね。

6. Shopping Cart

Shopping Cart

middlewareが登場しました。
middlewareはreducerがactionを処理する前後で、いろいろと便利なことをやってくれるものです。

redux-loggerはreducerがactionを処理したときに、reducer前のstateの値、actionの値、reducer後のstateの値をコンソールログに書き出してくれます。
開発中はめちゃくちゃ便利です。

redux-thunkは非同期処理を実現してくれます。
通常reducerはActionCreatorの戻り値であるactionを受け取ります。
しかしAjaxなどの非同期処理では、戻り値としてactionを返せません。
その場合は、非同期処理を実行する関数を戻り値として返しておけば、thunkがその関数を実行してくれます。

redux-thunkのREADMEにあるサンプルを見るとわかりやすいです。
incrementAsyncは非同期処理(1秒後に実行)なので、actionを返せません。
dispatch関数を引数とする関数を返しておけば、thunkがdispatchを引数にセットしてその関数を実行してくれます。
その関数の中で改めてactionをdispatchすることができます。

const INCREMENT_COUNTER = 'INCREMENT_COUNTER';

function increment() {
  return {
    type: INCREMENT_COUNTER
  };
}

function incrementAsync() {
  return dispatch => {
    setTimeout(() => {
      // Yay! Can invoke sync or async actions with `dispatch`
      dispatch(increment());
    }, 1000);
  };
}

thunkに渡す関数にはdispatchだけでなく、第2引数としてgetStateを渡すことができます。
非同期処理だけでなく、stateの値をつかってactionを生成したい場合にも有用です。

function incrementIfOdd() {
  return (dispatch, getState) => {
    const { counter } = getState();

    if (counter % 2 === 0) {
      return;
    }

    dispatch(increment());
  };
}

ちなみにthunkとは遅延評価(必要になったタイミングで評価される)関数のことで、Haskellでよく見かけます。
thinkの過去形のthunkから名付けたようです。(評価するときにはすでに考えた(thunk)から??)
JavascriptでのPromiseパターンに似ていますが、promiseはまだ評価されていない「状態」のことです(thunkはあとで評価する「関数」)。

このExampleではmiddleware以外にもReduxでよく使われるパターンが盛り込まれているようです。

  • IDのついたエンティティをstateで管理する方法
  • reducerを分割する方法(cart redudcer は addedIds と quantityByIdを組み合わせている)
  • stateの構造を知っているreducer内に、stateからデータを取り出す機能を実装する方法(reducer内に定義したgetCartProductsをつかってmapStateToPropsでstateからprops用のデータを取り出している)

これらのポイントを意識してソースを読むと理解が深まるでしょう。

ところで、CartContainerとProductContainerがContainer Componentとなるのはいいとして、Appもcontainersディレクトリに置かれています。
Appはreduxのデータフローにまったく関係ないので、Presentational Componentのような気がするのですが。
Containerを保持するためのContainerということなのでしょうか。

7. Tree View

Tree View

Componentの再利用とネストの例です。
効率よく変更のレンダリングをしてくれますよという、ReduxというよりReactの利点をよく示した例です。
データ(state)を一元管理しているのはReduxの良さで、データに変更が加わったときにViewのどこを変えればいいかということを開発者が気にしなくていいのはいいですね。
開発者から見たら、データを元にView(仮想DOM)を一から再作成してるイメージです。

8.Async

Async

非同期通信(AJAX)の例です。
Shopping Cartと同じく、middlewareにredux-thunkを使っています。

処理中の状態isFetchingをstateで管理して、処理中かどうかでViewを切り替えています。

function fetchPosts(reddit) {
  return dispatch => {
    dispatch(requestPosts(reddit))
    return fetch(`https://www.reddit.com/r/${reddit}.json`)
      .then(response => response.json())
      .then(json => dispatch(receivePosts(reddit, json)))
  }
}

fetchPostsではthunkでrequestPostsを実行して、処理中のアクション(type=REQUEST_POSTS)を生成します。
fetch後にreceivePostsを実行して、処理後のアクション(type=RECEIVE_POSTS)を生成します。

非同期通信の典型的なパターンがよく理解できる例です。

9.Universal

Universal

Counterをサーバレンダリングしています。
クライアントでReactDOM.render として仮想DOMを生成する代わりに、サーバでReactDOMServer.renderToStringとすればHTMLを生成することができます。
サーバレンダリングしない場合はdivタグ一つ返すのに比べて、サーバでHTMLを生成することでSEOに効果的らしいです。(Googleはjavscriptを解釈できるのであまり関係ないかもしれませんが)

サーバでもクラインとでもComponentは普遍的に使えるよということで、Universal。

10.Real World

Real World

これはいろいろな工夫(設計)がされています。

  • AJAXのレスポンスJSONはstateの構造にあわなかったりするので、normalizrでJSON構造を変更
  • AJAXのAPI callをmiddlewareとして共通化
  • 読み込んだデータを部分的に表示(RepoPageではレポジトリの情報とレポジトリにstarをしたユーザ一覧とを別々にfetchして表示)
  • ページネーション
  • レスポンスのキャッシュ
  • エラーメッセージの表示
  • クライアントサイドルーティング(URLに応じてViewを切り替える)
  • redux-devtoolsの使用(なかなかの高機能だが、個人的にはredux-loggerで十分)

・クライアントサイドルーティングについて

react-routerを使えば簡単に実現できます。

import React from 'react'
import { Route } from 'react-router'
import App from './containers/App'
import UserPage from './containers/UserPage'
import RepoPage from './containers/RepoPage'

export default (
  <Route path="/" component={App}>
    <Route path="/:login/:name"
           component={RepoPage} />
    <Route path="/:login"
           component={UserPage} />
  </Route>
)

こんな感じでルーティングをつくってあげれば、URLに応じてComponentの表示を切り替えてくれます。
loginやnameがownProps.paramsに入ってくるので、mapStateToPropsでpropsに反映させてあげればいいです。

function mapStateToProps(state, ownProps) {
  // We need to lower case the login/name due to the way GitHub's API behaves.
  // Have a look at ../middleware/api.js for more details.
  const login = ownProps.params.login.toLowerCase()
  const name = ownProps.params.name.toLowerCase()

URLの変更自体は何のアクションもdispatchせず、reducerも動かなければstateも変更がありません。
URLの変更をstateに反映させたい場合は、react-router-reduxを使うと便利です。
typeがLOCATION_CHANGEであるアクションがdispatchされたり、selectLocationState関数を用意しておけばstateの値に応じてURLを変更してくれたりします。

・API Callのmiddlwareについて

AJAXではリクエストを送信したとき(通信中)、レスポンスを受け取ったとき、エラーを受け取ったときにいろいろと処理してstateに反映させたくなります。
Asyncの例であったように、毎回3つのActionCreatorを用意してもいいのですが(Asyncの例ではリクエストとレスポンスの2つでエラーは考えていない)、全AJAX共通なのでapiとして自作でmiddlewareにしています。

これを真似してつくってもいいのですが、redux-api-middlewareというライブラリをつかえば同じことをやってくれます。

import { CALL_API } from `redux-api-middleware`;
fetch() {
  return
  {
    [CALL_API]: {
      endpoint: 'http://www.example.com/api/users',
      method: 'GET',
      types: [
        {
          type: 'REQUEST',
          meta: { source: 'userList' }
        },
        'SUCCESS',
        'FAILURE'
      ]
    }
  }
}
[CALL_API]というアクションを生成してdispatchすれば、endpointにリクエストを投げて、リクエスト時とレスポンス時とレスポンエラー時にそれぞれ指定したアクションをdispatchしてくれます。

Real Worldまでたどり着ければ、AJAXやキャッシングやクライアントサイドルーティングを駆使してたいていのReact/Reduxアプリケーションは作れてしまうでしょう。

大掛かりになればなるほど、actionとreducerとstateの設計がとても大切です。(Shpping Cartが参考になる)
stateを適切に管理できていれば、UIはReactのおかげでかなり楽できます。

LINEで送る
Pocket

コメントを残す