tomoima525's blog

Androidとか技術とかその他気になったことを書いているブログ。世界の秘密はカレーの中にある!サンフランシスコから発信中。

ReactNativeで理解しておくと良いReduxとMiddlewareのフローを理解する

f:id:tomoima525:20170424163521p:plain

(5/29/2017追記 ‘必要不可欠’ とタイトルに書いていたら'必要不可欠でない'と指摘を受けました。なんらかのデータフローの仕組みは必要だけどReduxである必要はないのでタイトル修正しました)
最近ReactNativeをちょこちょこ書いています。アプリ向けのReactNativeを書くにあたって理解がのぞましいのがデータフローの仕組みであるRedux、及び様々な処理を仲介するMiddlewareです。小さなアプリをつくってみて一通り把握したので、整理も兼ねて初めてReact-Reduxを触れる時にどの辺を見ればよいかまとめてみます。
作ったのはChuckNorris FactsのJokeを検索して表示するアプリです。

github.com

デモ動画

ざっくり仕様を述べると

  • ReactNative + Redux + RxJs
  • 検索ワードを投げる、ローディング、結果表示のステートをReduxで管理
  • https://api.chucknorris.io/APIをMiddlewareであるredux-observable経由で叩いて取得
  • APIレスポンスは RxJsでストリームをハンドル

という風になっています。

Reduxとは

すでにご存知な方も多いですが一応述べておくと、ステート管理のフレームワーク及びそれを実現するライブラリです。 ここで色々説明するよりRedux本家のページが分かりやすいです。
チュートリアルも充実しています。但しReactNativeではなくReactで書かれているため、自分のようにJavascript自体があまり馴染みが無い場合、ReactNativeとの違いに戸惑うこともあるかもしれません。

あとこちらのマンガでわかるReduxもおすすめです。

code-cartoons.com

Reduxの大まかな流れは以下になります。

(1) ReactコンポーネントからActionをコール
(2) ActionをStoreにあるReducerに渡す(dispatchと呼ばれる)
(3) Reducerがステートを更新してReactに返す(React側はコンテナを経由してコンポーネントにステートを渡す)

Reduxが優れているところ

上記の流れを見ると分かる通り、ステートで挙動が制御されるので、Atomic Design pattern である Reactと非常に相性が良いです。
React(ReactNative)はUIコンポーネントにすぎず、ある状態を渡すとそれを愚直に表示するだけのものです。その状態を綺麗に管理できるのがReduxになります。

余談ですが、ReduxのAPIリファレンスをみると登場するメソッドが6つしかないです。インタフェースよりも仕組みを理解することが重要なライブラリですね。

Middlewareとは

Reduxの非常に優れた仕様の一つで、ActionからReducerの間に発生する処理をつかさどる部分になります。上の流れで言うと(2)の部分で発生する処理をハンドルします。例えばAPIリクエストであったり、ログ処理を行ったりする部分です。
Middlewareの特徴としてはReduxを離れて様々なサードパーティのエクステンション等に処理を託すことができる点があります。サードパーティのMiddlewareとしてはredux-observable,redux-saga,redux-thunkなどそれぞれ色々な特徴を持ったものがあります。

ReactNative + Redux プロジェクトで見るべきところ

上記を踏まえた上で、ReactNativeのコードを追う場合に見るべきところは以下になるかなと思います。

トップのindex.js

ここではReducerとMiddlewareからStoreを生成し、アプリに設定するコードがあります。自分のアプリですと、index.anroid.js にあたります。

import chuckNorris from './js/reducers';
import chuckEpic from './js/epics';

// Middlewareを生成
const epicMiddleware = createEpicMiddleware(chuckEpic);
// Storeを生成 
const store = createStore(chuckNorris, applyMiddleware(epicMiddleware));

export default class ChuckNorrisViewer extends Component {
  render() {
    return (
      <Provider store={store}>// Appに設定
        <App/>
      </Provider>
    );
  }
}

Epics

Middlewareに託された各処理のことをEpicと呼びます。Epicsフォルダを見るとどのような処理がMiddlewareでハンドルされているかわかります。./js/epics/index.js を見ると

import { getNorris } from './posts';
const chuckEpic = combineEpics(getNorris);

と書かれていて、このアプリでは一つのEpicしか設定されてないとわかります。 さらにimportされた post.js を見てみると、

import {receivePosts} from '../actions'; //Action Creator
import {REQUEST_POSTS} from '../actions'; // Action Type
export const getNorris = action$ => {
  return action$.ofType(REQUEST_POSTS).mergeMap(action => {
    return ajax.getJSON(`https://api.chucknorris.io/jokes/search?query=${action.payload}`).map(response => receivePosts(response));
  });
};

ActionがREQUEST_POSTSであった場合にAPIリクエスト処理を行い、それを再度Action Creatorに返しているのがわかります。

Reducers

今度はReducersを見てみます。Reducersにはステートがどのように更新されているかが書かれています。./js/reducers/index.js を覗いてみると

const chuckNorris = combineReducers({
  searchResult,
  postByKey,
  visibilityFilter,
});

3つのReducerが登録されていることがわかります。例えばsearchResult.jsを見てみると

const searchResult = (state = {}, action) => {
  switch (action.type) {
    case RECEIVE_POSTS:
      return {
        ...state,
        items: action.payload
      };
  }
};

となっていて、RECEIVE_POSTS というActionを受け取った場合はpayload入れたステートを再度生成して返しているとわかります。なお ...stateObjectSpreadと呼ばれるSyntaxで、要は元のステートをコピーして新しいステートを生成しています。

Action

(2) ActionをStoreにあるReducerに渡す(dipatchと呼ばれる) の部分にあたります。どのようなActionがあるかというのも全体像を知る上で重要です。actions/index.jsを見てみると

export const RECEIVE_POSTS = 'RECEIVE_POSTS';
export const REQUEST_POSTS = 'REQUEST_POSTS';

//リクエストする時のAction
export const requestPosts = ( key ) => {
  return {
    type: REQUEST_POSTS,
    payload: key,
    isFetching: true,
  }
};

//結果を受け取った時のAction
export const receivePosts = ( response ) => {
  return {
    type: RECEIVE_POSTS,
    payload: response.result,
    isFetching: false,
  }
};

ActionTypes.jsとAction.jsを分ける場合もあるようですが、今回は同じファイルに書いています。 Actionにどのようなタイプがあり、それぞれのActionが取る値がわかるかと思います。

React Container

今度は各画面でどのようにReact側にステートが渡されて描画されるかを見ます。ContainerはReactにおいてどのように処理が行われるか(そして実際のUIコンポーネントに渡すか)を決めます。詳しくはここがわかりやすいです。
“どのように処理が行われるか” という言葉からも分かる通り、Reduxとも縁が深いところになります。

検索ワードを入力するContainerである containers/Input.js を見てみると

import { requestPosts } from '../actions';
...
const mapDispatchToProps = (dispatch) => {
  return {
    onButtonPress: bindActionCreators(requestPosts,dispatch),
  };
}
export default connect(null, mapDispatchToProps)(Input);

という処理があります。(1) ReactコンポーネントからActionをコールの部分で、これがActionを呼ぶ時のおまじないになっています。Storeに渡すdispatchにpropsで定義された値を接続しているわけです。ここでは onButtonPress という値が requestPosts をコールするようになります。
次に検索結果を表示するcontainers/ResultList.jsを見てみると、今度は

const mapStateToProps = (state) => {
  return {
    result: state.searchResult,
    visibilityFilter: state.visibilityFilter,
  }
}
export default connect(mapStateToProps)(ResultList);

という処理が見えます。これが(3) Reducerがステートを更新してReactに返すの部分で、Reducerから受け取ったステート更新をpropsに渡して描画を行うための手続きになります。ここでは searchResultvisibilityFilter という2つのReducerによるステートを受け取っています。

これでアプリ内の一通りの処理が追えたかと思います。

まとめ:Reduxをどう理解していけばよいか

自分にとってReduxの最初のつまづきははステートがどこから渡されて、どう処理されて更新していくのかが初見だと理解しにくい点でした。一度アプリを作ってconsoleにログを出しつつステートの変化を追うと、理解が深まったのでおすすめです。