最近ReactNativeをちょこちょこ書いています。アプリ向けのReactNativeを書くにあたって理解が不可欠になるのがデータフローの仕組みであるRedux、及び様々な処理を仲介するMiddlewareです。小さなアプリをつくってみて一通り把握したので、整理も兼ねて初めてReact-Reduxを触れる時にどの辺を見ればよいかまとめてみます。
作ったのはChuckNorris FactsのJokeを検索して表示するアプリです。
デモ動画
昨日のReactNativeアプリ続き。Reduxにローディングのステートも追加してみた。iOSとAndroidでも想定通り動く。 pic.twitter.com/js9yQNokuB
— Tomoaki Imai (@tomoaki_imai) 2017年4月24日
ざっくり仕様を述べると
- ReactNative + Redux + RxJs
- 検索ワードを投げる、ローディング、結果表示のステートをReduxで管理
- https://api.chucknorris.io/ のAPIをMiddlewareであるredux-observable経由で叩いて取得
- APIレスポンスは RxJsでストリームをハンドル
という風になっています。
Reduxとは
すでにご存知な方も多いですが一応述べておくと、ステート管理のフレームワーク及びそれを実現するライブラリです。
ここで色々説明するよりRedux本家のページが分かりやすいです。
チュートリアルも充実しています。但しReactNativeではなくReactで書かれているため、自分のようにJavascript自体があまり馴染みが無い場合、ReactNativeとの違いに戸惑うこともあるかもしれません。
あとこちらのマンガでわかるReduxもおすすめです。
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入れたステートを再度生成して返しているとわかります。なお ...state
は ObjectSpreadと呼ばれる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に渡して描画を行うための手続きになります。ここでは searchResult
とvisibilityFilter
という2つのReducerによるステートを受け取っています。
これでアプリ内の一通りの処理が追えたかと思います。
まとめ:Reduxをどう理解していけばよいか
自分にとってReduxの最初のつまづきははステートがどこから渡されて、どう処理されて更新していくのかが初見だと理解しにくい点でした。一度アプリを作ってconsoleにログを出しつつステートの変化を追うと、理解が深まったのでおすすめです。