ryhmrt’s blog

意識低い系プログラマの雑記

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

react-router-redux 4.0.0 向けに新しいのを書きました

-- 以下は古いもの --

Redux way では描画に必要なデータは props 経由で渡すことになります。 画面遷移時にデータの読込どうしようかと少し悩みましたが、今のところ、URLの変更を検知して、そのURLに対応したAPIを叩くという方向で進めています。

React RouterRedux の繋ぎとして react-router-redux を使っていまして、今のところ以下のような感じです。

import { createStore, applyMiddleware, combineReducers } from 'redux';
import thunkMiddleware from 'redux-thunk';
import createLogger from 'redux-logger';
import { syncHistory, TRANSITION, UPDATE_LOCATION } from 'react-router-redux';

import reducer from 'reducers';

import { loading, updateData } from 'actions/common';
import findAPI from 'apis';

const dataFetchMiddleware = store => next => {
    // Action type for initial load
    const INITIAL_LOAD = 'INITIAL_LOAD';
    // Utility method for asynchronous action dispatch
    function dispatchAsync(dispatch, action) {
        Promise.resolve(action).then(dispatch);
        return action;
    }
    // Utility method for API call
    function updateDataFromAPI(location) {
        // Call API
        findAPI(location, store)(data => {
            store.dispatch(updateData(data));
            next(loading(false));
        });
    }
    // Dispatch initial action asynchronously
    dispatchAsync(store.dispatch, {type:INITIAL_LOAD, payload:location});
    // Return core middleware function
    return action => {
        // Handle first screen load
        if (action.type === INITIAL_LOAD) {
            updateDataFromAPI(action.payload);
            return next(action);
        }
        // Activate loading screen before transition
        if (action.type === TRANSITION) {
            // This loading action is for user's action
            next(loading(true));
            // Dispatch original action to next middleware asynchronously to freeze screen before transition
            return dispatchAsync(next, action);
        }
        // Fetch data on update location
        if (action.type === UPDATE_LOCATION) {
            // This loading action is for browser's history action
            next(loading(true));
            updateDataFromAPI(action.payload);
            // Dispatch original action to next middleware asynchronously to freeze screen before transition
            return dispatchAsync(next, action);
        }
        return next(action);
    };
}

export default function configureStore(initialState, history) {
    const loggerMiddleware = createLogger();
    const reduxRouterMiddleware = syncHistory(history);
    return createStore(reducer, initialState, applyMiddleware(thunkMiddleware, loggerMiddleware, dataFetchMiddleware, reduxRouterMiddleware));
}

store生成呼び出しは以下のようになってます。

import createBrowserHistory from 'history/lib/createBrowserHistory';
import configureStore from 'configure-store';

const browserHistory = createBrowserHistory();
const store = configureStore(initialState, browserHistory);

push 等のアクションからURL遷移する場合は、 TRANSITION が処理された後に UPDATE_LOCATION が処理されます。このとき React Router は最初の TRANSITION アクションでルーティングを変更してしまうようなので、TRANSITION が処理される前にローディング処理のアクションを投げています。

ブラウザの履歴操作でURL遷移する場合は TRANSITION が無く UPDATE_LOCATION だけが処理されます。そのため UPDATE_LOCATION が処理される前にもローディング処理を入れています。

loading(loadingStatus){type:'DISPLAY_LOADING', value:bool} といったアクションを返すメソッドです。最終的にこいつを処理する reducer がローディング中を示す state の値を書き換えます。ローディング中は props が変更されても画面が書き換わらないように、上っ面の shouldComponentUpdate でごにょごにょしてます。

ローディング処理の呼び出し後にURL遷移のアクション呼び出しを dispatchAsync(next, action); とかやっているのは、ローディングのアクションを処理してから先に一度レンダリング処理をして、画面をフリーズさせたかったからです。普通に呼び出すと、両方が処理されてからレンダリングが発生するので、データが無い状態で遷移先の画面が描画されてしまいます。

新規の生成したアクションの呼び出しが所によって next だったり store.dispatch だったりするのは、このミドルウェアの上にロガーがいるので、そいつに出すとうざいなと思った物は next に渡しているという次第です。ミドルウェアの順序重要。

最初の方で dispatchAsync(store.dispatch, {type:INITIAL_LOAD, payload:location}); と呼び出しているのは、一番最初のデータ取得処理をキックするためです。なんか汚い感じがしますが、とりあえず動いてます。