Open3

React 非同期処理と向き合う

oosukeoosuke

ReactでAPIなどでデータを取得する際に起きるウォーターフォール

コンポーネントマウント時にuseEffectなどでデータを取得するケース

function Parent() {
    const [user, setUser] = useState(null);

    useEffect(() => {
        fetchUser().then(user => setUser(user))
    }, []);

    if (!user) return <div>Loading user...</div>

    return (
        <div>{user.name}</div>
        <Child />
    )
}

function Child() {
    const [profile, setProfile] = useState(null);
    
    useEffect(() => {
        fetchProfile().then(profile => setProfile(profile))
    }, []);

    if (!profile) return <div>Loading profile...</div>

    return (
        <div>{profile.detail}</div>
    )
}

この時、Parentのデータの取得が終わらないとChildがマウントされないため、Childのデータ取得がはじまらない。

oosukeoosuke

なら一括で取得しようとやりがちなもの

const { useState } = require("react");

fetchData() {
    return Promise.all([
        fetchUser,
        fetchProfile
    ]).then(([user, profile]) => {
        return {user, profile};
    });
};

function Parent() {
    const [user, setUser] = useState(null);
    const [profile, setProfile] = useState(null);

    useEffect(() => {
        fetchData().then(data => {
            setUser(data.user);
            setProfile(data.profile);
        })
    }, []);

    if (!user) return <div>Loading user...</div>

    return (
        <div>{user.name}</div>
        <Child profile={profile} />
    )
}

function Child(profile) {

    if (!profile) return <div>Loading profile...</div>

    return (
        <div>{profile.detail}</div>
    )
}

全てのデータが取得されるまでLoading...

oosukeoosuke

Reduxを利用したmiddlewereでの処理

reduxではview => action => dispatch => reducer = store => viewというサイクルが一方通行で進む単方向データフローであるfluxというアーキテクトを元に生まれており、非同期処理などの副作用を含む処理はmiddlewere上で処理をすることが推奨されている。reduxにおいて、actionはpureなオブジェクトを返す、reducerによる値の更新は入出力の間に値の変化をはさまない純粋関数であること、store(状態)はread only読み取り専用であることで、複雑な状態管理をシンプルにし、かつバグをもたらさない堅牢なものとしています。

fluxアーキテクト

https://github.com/facebook/flux

個人的にはこのfluxという単方向データフローアーキテクトは昨今の状態管理方法としては最適解だと思っている

redux-saga

ES6 generatorの機能を使い、記述を簡潔、用意にテストも用意にする。
generatorという知らないと違和感しかない書き方にまずは慣れることが必要。

redux-sagaは特定のアクションに対して実行されるgenerator関数をあらかじめ登録、待機状態とします。
アクションがディスパッチされるとgenerator関数が起動、指定された処理を実行し、次のアクションをディスパッチする。このgeneratorの実行は直列、並列どちらも可能。

学習コストはそれなりですが(generator含む)、慣れてしまえば統一された書き方によってコード自体は見やすく、いろいろなアクションの構成を組み合わせることができる。
副作用となるロジックをsagaに閉じ込めることができる。

https://github.com/redux-saga/redux-saga

import { call, put, takeEvery, takeLatest } from 'redux-saga/effects'
import Api from '...'

function* fetchUser(action) {
   try {
      const user = yield call(Api.fetchUser, action.payload.userId);
      yield put({type: "USER_FETCH_SUCCEEDED", user: user});
   } catch (e) {
      yield put({type: "USER_FETCH_FAILED", message: e.message});
   }
}

function* mySaga() {
  yield takeEvery("USER_FETCH_REQUESTED", fetchUser);
}

function* mySaga() {
  yield takeLatest("USER_FETCH_REQUESTED", fetchUser);
}

export default mySaga;

redux-thunk

dispatcherにはpureなオブジェクトを渡すこととしていますが、redux-thunkはこちらに関数を渡すことを可能とします。thunkをmiddlewereとして登録すると、アクションがディスパッチされた際に、アクションオブジェクトが関数かどうかを判定します。関数であればdispatcherとstoreの値を読むことができるgetState関数を渡して関数を実行します。関数でなければ通常のreduxの処理通り、reducerへ処理が移ります。

学習コストは低く、導入も用意で、基本的なjsの知識があれば記述も可能。
アクションを関数に置き換える性質のため、その関数の中で自由度がかなり高く、しっかりルールを統一しないとThunkの処理が破綻しやすい。このアクションの処理が肥大化しがち。

https://github.com/reduxjs/redux-thunk

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)
  }
}

redux-observable Epic

名前の通り、ReduxでObservableを利用可能にするmiddlewere。
ここでいうObservableとはRxJSのストリームを指す。つまり、redux-observableはReduxでRxJSベースの処理記述が可能となるもの。

RxJSはリアクティブプログラミングを可能にするライブラリ。
リアクティブプログラミングとはあるデータを観測し、データに変化生じた際に、あらかじめ登録された操作をする。ストリームとは観測対象となるデータを指す。Pub/Subイベントモデル。

アクションを観測対象のデータ(ストリーム)として扱い、アクションの変化を観測、変化が生じた際に登録された処理を実行し、新たなアクションを返す処理となり、この一連をEpicという単位で扱う。
記述としてはnode.jsなどのfsモジュールなどにおけるstream APIと似ている。
Epicはアクションを受け取り、新たなアクションを返す。アクションイン、アクションアウトの形式で書かれる。

導入コストは高め、学習コストはsaga、thunk、epicの中で一番高いと思う。理由としてはRxJSにおけるリアクティブプログラミングの学習コスト。そこを押さえている場合は、記載した3つのmiddlewereの中で一番汎用性があり、さまざまなケースにも対応できると個人的に思う。

https://redux-observable.js.org/docs/basics/Epics.html

import { ajax } from 'rxjs/ajax';

// action creators
const fetchUser = username => ({ type: FETCH_USER, payload: username });
const fetchUserFulfilled = payload => ({ type: FETCH_USER_FULFILLED, payload });

// epic
const fetchUserEpic = action$ => action$.pipe(
  ofType(FETCH_USER),
  mergeMap(action =>
    ajax.getJSON(`https://api.github.com/users/${action.payload}`).pipe(
      map(response => fetchUserFulfilled(response))
    )
  )
);

// later...
dispatch(fetchUser('torvalds'));

この3つをものすごくざっくりイメージで書くと

saga
Action => Dispatch => saga => Reducer => Store => View

thunk
thunk => Action => Dispatch => Reducer => Store => View

epic
Action => epic => Dispatch => Reducer => Store => View