ryhmrt’s blog

フィリピンから日本に戻った意識低い系プログラマの雑記

ページ遷移時にAPIを叩いてデータを読み込む with react-router-redux 4.0.0

react-router-redux を 4.0.0 にしたらメソッド名とか中の処理とかけっこう変わってました。

前に書いたURLのフック処理がいろいろ駄目になっていたので、4.0.0でどんな処理になっているのか、ざっと追って、新しくコードを書き直してみました。

react-router-redux が何をやっているのか

最新の 4.0.0 では react-router-redux がラッピングした history オブジェクトを React Router に渡すようになっています。

このラッピングされた history は listen イベント登録が書き換えられており、store に格納されているURLが変更されると、その情報をパラメータに listener がトリガーされるようになっています。

流れを3ステップで説明すると以下のようになります。

  1. オリジナルの history のURL変更イベントで LOCATION_CHANGE アクションを dispatch する
  2. routerReducer が LOCATION_CHANGE を受けとって、新しい URL 情報を store に格納する
  3. react-router のURL監視用 listener が呼び出され、ルーティングが書き換わる

3.0.0からの違い

以下のコードは react-router-redux の README.md にあるもの。

import React from 'react'
import ReactDOM from 'react-dom'
import { createStore, combineReducers, applyMiddleware } from 'redux'
import { Provider } from 'react-redux'
import { Router, Route, browserHistory } from 'react-router'
import { syncHistoryWithStore, routerReducer } from 'react-router-redux'

import reducers from '<project-path>/reducers'

// Add the reducer to your store on the `routing` key
const store = createStore(
  combineReducers({
    ...reducers,
    routing: routerReducer
  })
)

// Create an enhanced history that syncs navigation events with the store
const history = syncHistoryWithStore(browserHistory, store)

ReactDOM.render(
  <Provider store={store}>
    { /* Tell the Router to use our enhanced history */ }
    <Router history={history}>
      <Route path="/" component={App}>
        <Route path="foo" component={Foo}/>
        <Route path="bar" component={Bar}/>
      </Route>
    </Router>
  </Provider>,
  document.getElementById('mount')
)

以前は syncHistory で middleware が返ってきましたが、4.0.0 の syncHistoryWithStore は react-router-redux がラッピングした history を返してくるので注意が必要。

これを使わずにオリジナルの history を react-router に渡してしまうと、細かいところで変なことが起こります。要注意。

前のバージョンで syncHistory が返していた middleware は、routerMiddleware として別物になってオプション扱い。

というのも、この middleware は URL 変更のアクションを受けとって history に対して操作を行うためにあるのですが、そんな回りくどいことをせずに history を直接呼べば middleware 登録をする必要はないでしょうという話だと思います。

以下のコードは README.md に載っているもので、Redux のアクションを使ってURL遷移をするサンプルコードです。

import { routerMiddleware, push } from 'react-router-redux'

// Apply the middleware to the store
const middleware = routerMiddleware(browserHistory)
const store = createStore(
  reducers,
  applyMiddleware(middleware)
)

// Dispatch from anywhere like normal.
store.dispatch(push('/foo'))

しかし、history を直接使うならば以下のようにシンプルにできます。

import { browserHistory } from 'react-router'

browserHistory.push('/foo')

あと、前のバージョンでは index.js に全てがずらずらっと(とは言っても短いですが)書かれていたのが、4.0.0 ではファイルが細切れになっています。

ページ遷移のフック処理

現状、ページ遷移をフックしてAPIを呼び出す処理は以下のようになっています。

import { createStore, applyMiddleware } from 'redux';
import thunkMiddleware from 'redux-thunk';
import createLogger from 'redux-logger';
import { match } from 'react-router';
import { UPDATE_LOCATION } from 'react-router-redux';
import { loading, updateData } from 'actions/common';
import reducer from 'reducers';

const dataFetchMiddleware = routes => store => next => {
    // Utility method for API call
    function updateDataFromAPI(location) {
        match({ routes, location }, (error, redirectLocation, renderProps) => {
            if (error || redirectLocation) {
                return;
            }
            // Find API
            const route = renderProps.routes[renderProps.routes.length - 1];
            if (route.fetchAPI) {
                // Call API if it's available
                api(renderProps.params, renderProps.location.query, data => {
                    store.dispatch(updateData(data));
                    store.dispatch(loading(false));
                });
            } else {
                Promise.resolve(loading(false)).then(store.dispatch);
            }
        });
    }
    // Return core middleware function
    return action => {
        // Fetch data on update location
        if (action.type === LOCATION_CHANGE) {
            next(loading(true));
            updateDataFromAPI(action.payload);
        }
        return next(action);
    };
}

export default function configureStore(initialState, routes) {
    return createStore(reducer, initialState, applyMiddleware(thunkMiddleware, loggerMiddleware, dataFetchMiddleware(routes)));
}

URLとAPIのマッピングは routes の定義に書くことにしました。

match というユーティリティが react-router にあり、routes 定義と location を渡すと、マッチしたルーティング情報を返してくれます。

末端の route 定義に fetchAPI というキーでデータ取得のための関数を定義するようにして、それをここで取得するようにしました。

注意点は、match が onEnter を呼び出しますので、onEnter で副作用のあることをしないように。そもそも、onEnter はリダイレクト処理くらいしかやってはいけないのだと思います。