196
179

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

reduxを試してみた(5日目) - ajaxを使ってUIを構築する(reduxにおける非同期の制御)

Last updated at Posted at 2015-09-17

このページは作業ログです。やったことをつらつら書くだけなのでまとめません。あしからず。

対象読者

  • 主に自分

今日のゴール

  • jQueryではない方法を使ってajaxを使えるようになっていること
  • Reactのコンポーネントを非同期リクエストで構築できるようになっていること

モチベーション

  • React.jsはビューのみをサポートしたライブラリ。サーバーとの通信部分はなんも用意していない
  • チュートリアルではjQueryを使ってサーバーと通信してた。でもjQueryはその他もろもろいろいろ機能がある。Virtual DOMとの相性がありそうなので、jQuery自体導入せずに済ませられるのならそうしたい。

調べてみた

A Comparison of JavaScript HTTP Libraries for the Browserという記事によると、Ajaxリクエストを実現するには下記の方法がある。

また、非同期にfetchするためのAPIであるFetch APIの標準化が進んでいる。JackさんのFetch API 解説、または Web において "Fetch する" とは何か?という記事に詳しく書かれている。window.fetchというAPIが最近のブラウザにはあるらしい。Fetch APIを実現するPolifillやライブラリとして下記がある。

今日はaxiosを使って構築しているReact.jsのアプリケーションに組み込んでみる。

axiosの選定理由

  • Fetch APIを使えばpostリクエストもできるが、Fetch APIの目的はリソースを取得することだ。
  • Fetch API自体もXMLHttpRequestよりも下位の層のAPIとして位置づけられている。そのため、リクエスト時のクッキーの添付など自前で実装する必要があり、面倒くさそう。
  • XMLHttpRequest自体を直接煽るのはブラウザ互換が難しそう。
  • superagentとaxiosを比較すると、axiosはPromissで返ってくるので、Callback地獄になりにくそうに見えた。
  • 反面、superagentは長く使われているのでプラグインによるエコシステムが構築できている。

Reactのコンポーネントをajaxを使って構築する

トライしてみたけど実現できず。やってみたら下記の疑問が浮かんできた。

  • ajaxのリクエストはactionでやるのか?reducerじゃないのか?
  • store.dispatch(action)をしたら対応するreducerを呼んでくれるはずが、呼ばれないのはなぜか?

このあたりに入ってくると、reduxのtutorialの3章Advancedを理解する必要がある。reduxのドキュメント3. Advancedを読む。

reduxのドキュメント「3. Advanced」を読む

メモというか、大事なところかつわかりにくいところなので、本文を妙訳する。

3.1 Acync Actions

ドキュメント

Reddit APIを使ったサンプル・アプリケーションの解説。非同期な処理を行う時に、重要なタイミングが2つある。1つは非同期な処理を開始時、もう1つは非同期な処理のレスポンス(かタイムアウト)時だ。そうすると、3つの種類のアクションが必要になる。

リクエスト開始時にreducerを呼び出すためのaction

isFetchingフラグを立てるためのアクション。isFetchingの制御ができればspinnerを表示することができる。

リクエストが成功した時にreducerを呼び出すためのaction

このreducerは取得したデータをstateに反映したり、isFetchingフラグをリセットする。そうすればspinnerの表示を終了したり取得したデータをUIに表示したりできる。

リクエストが失敗した時にreducerを呼び出すためのaction

このreducerはisFetchingフラグをリセットし、エラーメッセージをstateに追加する。そうすることでspinnerを非表示にし、エラーメッセージをUIに表示する。

成功時と失敗時のActionの構造の作り方はアプリケーションで統一させる。

{ type: 'FETCH_POSTS' }
{ type: 'FETCH_POSTS', status: 'error', error: 'Oops' }
{ type: 'FETCH_POSTS', status: 'success', response: { ... } }

のようにstatusを追加するか、

{ type: 'FETCH_POSTS_REQUEST' }
{ type: 'FETCH_POSTS_FAILURE', error: 'Oops' }
{ type: 'FETCH_POSTS_SUCCESS', response: { ... } }

のようにTypeを分ける。

個人的な意見

REQUESTと完了時のActionを分け、完了時のアクションでステータスを扱う形が良いように思う。

{ type: 'REQUSET_POSTS' }
{ type: 'FETCHED_POSTS_FAILURE', error: 'Oops' }
{ type: 'FETCHED_POSTS_SUCCESS', response: { ... } }

こんな感じ。サンプルアプリの実装だとこのパターンだったような??

Action Creatorの同期呼び出し

リクエスト時もレスポンス時も同期的にActionを作っていい。

例えばRedditを選択するActionは下記のように実装する。

export const SELECT_REDDIT = 'SELECT_REDDIT';

export function selectReddit(reddit) {
  return {
    type: SELECT_REDDIT,
    reddit
  };
}

このAction CreatorをUIなどでstore.dispatch()に渡す。この呼出しは同期呼び出しでOK。

State(状態)の構造設計

basic tutorialと同様、実装に入る前にStateの構造設計をすませておく。非同期の場合は何を状態として定義すべきか、また、単一のツリーにどうやって組み込むか、と言うあたりで初心者を混乱させがち。

例えばブログの投稿(posts)やフォロワー(follower)の取得など一覧を非同期で取得することを考えてみる。すると、どういった順番で表示すべきか、ということも必要になりそうだ。また、必要な部分だけフェッチし、残りはキャッシュをそのまま使うなどそれぞれを別々の状態として分けておきたくなるだろう。

サンプルのアプリケーションの'Reddit headlines'の場合は下記のようなデータになる。

{
  selectedReddit: 'frontend',
  postsByReddit: {
    frontend: {
      isFetching: true,
      didInvalidate: false,
      items: []
    },
    reactjs: {
      isFetching: false,
      didInvalidate: false,
      lastUpdated: 1439478405547,
      items: [{
        id: 42,
        title: 'Confusion about Flux and Relay'
      }, {
        id: 500,
        title: 'Creating a Simple Application Using React JS and Flux Architecture'
      }]
    }
  }
}

この構造から次のことがわかるだろう。

  • subreddit毎に情報を分けることでキャッシュができる。2回目に表示しようとした瞬間にデータを更新できる。また、必要になるまでデータを再取得しなくて済む。これらの要素が全てメモリに入っていることを気にしなくてOK.(ただしめちゃくちゃ大量の要素を扱う場合や、ユーザーがほとんどタブを閉じない場合は除く。クリーンアップの事はほぼ考えなくて大丈夫。)
  • リスト毎にisFetchingを用意すれば、それぞれ「読込中」をコントロールできる。didInvalidateがあればデータが無効になっている事を示せる。lastUpdated があれば前回読み込んだ日時を記録できるし、itemsという項目で実際のデータを保持できる。ふつうのアプリケーションなら、fetchedPageCountnextPageUrlでペジネーションを制御できる。

Note: ネストしたエンティティ

今回は、ペジネーションの情報と受信した情報を一緒に管理するようにした。しかし、このやり方は各々の要素が参照しあうような場合や、ユーザーがデータを編集するような場合はうまくいかなくなる。ユーザーがfetchしたデータを編集したいが、state tree上のいろんな場所にデータが存在する事を想像してほしい。実装が面倒になっていくが目に浮かぶだろう。

もしエンティティがネストしていて、その要素をユーザーが編集する場合、stateの中で分離しつづけるべきだろう。ペジネーションに使う場合、IDだけで参照できるようにすべきだ。これにより、個々のエンティティの更新をしやすくする。これを実践したのがreal world exampleだ。APIのレスポンスを正規化するため、normalizrを使っている。このやり方を採用すると、アプリケーションの状態は下記のようになるだろう。

state
{
  selectedReddit: 'frontend',
  entities: {
    users: {
      2: {
        id: 2,
        name: 'Andrew'
      }
    },
    posts: {
      42: {
        id: 42,
        title: 'Confusion about Flux and Relay',
        author: 2
      },
      100: {
        id: 100,
        title: 'Creating a Simple Application Using React JS and Flux Architecture',
        author: 2
      }
    }
  },
  postsByReddit: {
    frontend: {
      isFetching: true,
      didInvalidate: false,
      items: []
    },
    reactjs: {
      isFetching: false,
      didInvalidate: false,
      lastUpdated: 1439478405547,
      items: [42, 100]
    }
  }
}

このガイドではエンティティの正規化は行わない。より複雑なアプリケーションを開発する場合はこのやり方の採用を考えてみるとよい。

Actionの処理

ネットワークのリクエストと共にactionをdispatchするやり方を詳しく見ていく前に、reducerを先に見ていこう。

Note: Reducer Composition(複合Reducer)

compositeReducer()を使ったReducer Compositionに馴染みがない場合は、ベーシックガイドSplitting Reducer(Reducerの分割)にかかれているので、そちらを先に読むこと。

reducer.js
import { combineReducers } from 'redux';
import {
  SELECT_REDDIT, INVALIDATE_REDDIT,
  REQUEST_POSTS, RECEIVE_POSTS
} from '../actions';

function selectedReddit(state = 'reactjs', action) {
  switch (action.type) {
  case SELECT_REDDIT:
    return action.reddit;
  default:
    return state;
  }
}

function posts(state = {
  isFetching: false,
  didInvalidate: false,
  items: []
}, action) {
  switch (action.type) {
  case INVALIDATE_REDDIT:
    return Object.assign({}, state, {
      didInvalidate: true
    });
  case REQUEST_POSTS:
    return Object.assign({}, state, {
      isFetching: true,
      didInvalidate: false
    });
  case RECEIVE_POSTS:
    return Object.assign({}, state, {
      isFetching: false,
      didInvalidate: false,
      items: action.posts,
      lastUpdated: action.receivedAt
    });
  default:
    return state;
  }
}

function postsByReddit(state = {}, action) {
  switch (action.type) {
  case INVALIDATE_REDDIT:
  case RECEIVE_POSTS:
  case REQUEST_POSTS:
    return Object.assign({}, state, {
      [action.reddit]: posts(state[action.reddit], action)
    });
  default:
    return state;
  }
}

const rootReducer = combineReducers({
  postsByReddit,
  selectedReddit
});

export default rootReducer;

このコードには興味深い箇所が2つある。

  • ES6のcomputed property name(プロパティ名の計算)を使っている。これにより、Object.assignstate[action.reddit]を使って簡潔に記述できている。
part.js
return Object.assign({}, state, {
  [action.reddit]: posts(state[action.reddit], action)
});

これは下記と同等だ。

part.js
let nextState = {};
nextState[action.reddit] = posts(state[action.reddit], action);
return Object.assign({}, state, nextState);
  • 特定のpost一覧を管理するposts(state, action)を抽出している。これこそがreducer compositionであり、小さなreducerにどうやって分割したのかわかる実装例だ。この例では、ネストした要素の更新をpostsというreducerに委譲している。real world exampleではさらにパラメータを使ってペジネーションを制御するreducerを作るreducer factoryの実装方法を参考にできるだろう。

Action Creator の非同期呼び出し

最後に、上記に述べたネットワークにリクエストする同期action creatorの使い方を見ていこう。Reduxでの標準的なやり方はRedux Thunk middlewareを使うことだ。このミドルウェアはredux-thunkというパッケージにある。このミドルウェアがどのように動作するかは後述する;ここでは、読者が学ばなければならない重要な事だけ扱う。Redux Thunkミドルウェアを利用する時は、 action creatorはaction objectだけではなく、functionを返す事ができる。 これにより、action creatorはthunkとして扱える。1

action creatorがfunctionを返すと、Redux Thunkミドルウェアがそのfunctionを受け取り実行する。この関数は純粋関数でなくてもよい。非同期API呼び出しなど、副作用があってもよいのだ。この関数の中でactionをdispatchしてもよい。上述したようにactionを同期呼び出ししてもよい。

こういったthunkアクションの実装例がactions.jsだ。

actions.js
import fetch from 'isomorphic-fetch';

export const REQUEST_POSTS = 'REQUEST_POSTS';
function requestPosts(reddit) {
  return {
    type: REQUEST_POSTS,
    reddit
  };
}

export const RECEIVE_POSTS = 'RECEIVE_POSTS'
function receivePosts(reddit, json) {
  return {
    type: RECEIVE_POSTS,
    reddit,
    posts: json.data.children.map(child => child.data),
    receivedAt: Date.now()
  };
}

// thunk action creatorの最初の実装例
// 実装が異なるにもかかわらず、他のaction creatorと同じように利用できる。
// store.dispatch(fetchPosts('reactjs'));

export function fetchPosts(reddit) {

  // Thunkミドルウェアは関数をどのように扱うかわかっている。
  // dispatchメソッドの引数をそのまま関数に渡し、
  // その結果、action自体を実行できるようにする。

  return function (dispatch) {

    // 最初のdispatch: API呼び出しが開始されたと知らせるため、アプリケーションの状態は更新される。

    dispatch(requestPosts(reddit));

    // Thunkミドルウェアから呼び出された関数は、dispatchメソッドのreturn値として値をreturnできる。

    // この例では、実行を待たせるためpromiseを返している。
    // Thunkミドルウェアは必須ではないが、便利なので使っている。

    return fetch(`http://www.reddit.com/r/${reddit}.json`)
      .then(response => response.json())
      .then(json =>

        // 何度でもdispatchできる。
        // API呼び出しの結果としてアプリケーションの状態が更新される。

        dispatch(receivePosts(reddit, json))
      );

      // ネットワーク呼び出し中に起きたエラーのキャッチもできる。
  };
}

Note: fetchについて

この例ではfetch APIを使っている。fetch APIはネットワークのリクエストを行う新しいAPIでXMLHttpRequestの大部分のニーズを満たす代替手段だ。多くのブラウザはまだ実装していないので、isomorphic-fetchを使って実装することをオススメする。

// `fetch`を使う全てのファイルに宣言
import fetch from 'isomorphic-fetch';

isomorphic-fetchライブラリの内部では、クライアントサイドで実行するとwhatwg-fetchのポリフィルを使い、サーバーサイドで実行するとnode-fetchを使う。そのため、アプリケーションをユニバーサルにする場合でも、書き換える必要がない。

fetchポリフィルはPromiseポリフィルを前提にしていることを忘れてはならない。Promiseポリフィルを使う簡単な方法は、他のどのコードも実行する前にBabelのES6ポリフィルを有効にすることだ。

// アプリケーションのどんなコードも実行する前に一度だけ実行する
import 'babel-core/polyfill';

dispatchの機構にRedux Thunkミドルウェアを取り込むにはどうすればよいのだろうか?下記のようにapplyMiddleware()
を呼べばよい。

index.js
import thunkMiddleware from 'redux-thunk';
import createLogger from 'redux-logger';
import { createStore, applyMiddleware } from 'redux';
import { selectReddit, fetchPosts } from './actions';
import rootReducer from './reducers';

const loggerMiddleware = createLogger();

const createStoreWithMiddleware = applyMiddleware(
  thunkMiddleware, // lets us dispatch() functions
  loggerMiddleware // neat middleware that logs actions
)(createStore);

const store = createStoreWithMiddleware(rootReducer);

store.dispatch(selectReddit('reactjs'));
store.dispatch(fetchPosts('reactjs')).then(() =>
  console.log(store.getState())
);

thunkの良いところは実行結果をdispatchできるところだ。

actions.js
import fetch from 'isomorphic-fetch';

export const REQUEST_POSTS = 'REQUEST_POSTS';
function requestPosts(reddit) {
  return {
    type: REQUEST_POSTS,
    reddit
  };
}

export const RECEIVE_POSTS = 'RECEIVE_POSTS'
function receivePosts(reddit, json) {
  return {
    type: RECEIVE_POSTS,
    reddit,
    posts: json.data.children.map(child => child.data),
    receivedAt: Date.now()
  };
}

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

function shouldFetchPosts(state, reddit) {
  const posts = state.postsByReddit[reddit];
  if (!posts) {
    return true;
  } else if (posts.isFetching) {
    return false;
  } else {
    return posts.didInvalidate;
  }
}

export function fetchPostsIfNeeded(reddit) {

  // Note: この関数が返すfunctionは、次にdispatchする関数を決めるgetState()も受け取る。
  // Redux ThunkミドルウェアがdispatchとgetStateを渡す。

  // キャッシュしている値が既にあるのであれば、
  // ネットワークリクエストを避けるのに使える。

  return (dispatch, getState) => {
    if (shouldFetchPosts(getState(), reddit)) {
      // thunkからthunkを呼び出せる!
      return dispatch(fetchPosts(reddit));
    } else {
      // 下記コードを呼び、wait forには何もないことを知らせる
      return Promise.resolve();
    }
  };
}

これにより、非同期なコード呼び出しフローを、全く同じ振る舞いを保ったままより洗練された書き方になる。

index.js
store.dispatch(fetchPostsIfNeeded('reactjs')).then(() =>
  console.log(store.getState());
);

Note: サーバーサイドレンダリング

非同期なAction Creatorは特にサーバーサイドレンダリングに便利だ。storeを作成したり、アプリケーションのあるセクション全体のデータをfetchするための非同期のaction creatorをまとめていくつかdispatchする非同期のaction creatorをdispatchしたり、処理が完了しPromiseがreturnされた後にだけrenderしたりできる。レンダリングする前からstoreを必要な状態にできる。

Thunkミドルウェア以外にもReduxで非同期のactionをうまくやりとりする方法がある。functionではなくPromiseを使ったredux-promiseredux-promise-middlewareも利用できる。redux-rxを使ってObservablesにdispatchすることもできる。real world exampleでやっているようにAPIの呼び出し定義を実装したカスタムミドルウェアを実装することもできるだろう。ミドルウェアを使う、使わないにかかわらず、いくつかの選択肢をためしてみて慣習を学び、それに従おう。

UIとの接続

非同期のactionの呼び出しと同期のactionの呼び出しには違いはないので、ここでは詳細には述べない。ReactのコンポーネントからReduxを使ったイントロダクションを読みたければUsage with Reactを見よう。今回取り上げたソースコードの完全版はExample: Reddit APIだ。

次のステップ

非同期のactionがReduxのflowにどう合うのか、次はAsync Flowを取り上げる。

3.1の感想とまとめ

  • redux-thunkは遅延評価ができるミドルウェアなのか。
  • ソフトウェアのコンテクストでthunk(サンク)という言葉の意味を初めてしった。
  • 高階関数もJavaScriptでも割と登場するね。
  • isomorphic-fetchを使っている理由が書かれていた。XMLHttpRequestよりもよりプリミティブなAPIという解説があったから自分は使わないつもりだけど、ユニバーサルなアプリを書く時に楽できそうなのはいい。
  • 全文訳してみると、ざっくり読むよりもより深く理解できた気がする。

次回

  • Async FlowやMiddlewareも訳したい。
  1. 遅延評価のこと

196
179
0

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
196
179

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?