プロジェクトがReact、React-Router、Reduxを使用して開発されているため、フロントエンドレンダリング方式を採用しました(SSRを採用していればこの問題は発生しませんでした)。
問題
なので、各ページにアクセスするたびに、ページデータを取得するための初期リクエストを送信し、その後ページを再レンダリングする必要があります。そのため、以前のページのリクエストからロード完了までのフローは次のようになっていました。
Route match -> ComponentWillMount -> render -> ComponentDidMount -> dispatch(init())-> render -> componentDidUpdate
まずルーティングがマッチし、その後コンポーネントのロード準備が行われます。Reducer
内の初期 state
を使用して render
が実行され、ComponentDidMount
イベントがトリガーされます。このイベント内で、ページ初期化リクエストを実行する Action
を dispatch
し、リクエストが成功するとコンポーネントの再レンダリングがトリガーされます。
ご覧の通り、最終的なページを表示するにはコンポーネントが2回再レンダリングされる必要があります。1回目はフロントエンドのreducer
にハードコードされた initialState
データを使用してレンダリングされ、2回目はバックエンドデータが取得された後にレンダリングされます。
そのため、時折画面がちらつくことがあり、ユーザーエクスペリエンスは非常に悪いものでした。
私たちの要件は、ページにアクセスした後、バックエンドから取得した最新データでレンダリングされたページが表示され、ページ全体が1回だけ render
されることです。
では、この問題をどのように解決すればよいでしょうか?
解決策
この問題を解決するには、データをロードしてからページコンポーネントをマウントする必要があります。そのため、データロードのタイミングが非常に重要になります。
従来のサーバーサイドレンダリングページの方法を参考にすると、このタイミングはルーティング内で行うのが最も適切でしょう。
具体的には、プロジェクトでは react-router
の onEnter
イベント内でページの初期化リクエストを実行し、すべてのデータリクエストが成功した後にこのページをロードします。
ページ全体のロードフローは次のようになります。
Route match -> dispatch(init()) -> ComponentWillMount -> render -> ComponentDidMount
具体的なコードは以下の通りです。
HomeAction.js
import { createAction } from "redux-actions";
import { HOME_INDEX } from "../../config/apis.js";
import createAsyncAction from "../../utils/createAsyncAction.js";
import initAPI from "../../utils/initAPI.js";
export const InitActionList = createAsyncAction("home/init");
export const FormChange = "home/formChange";
export const FormFieldChange = "home/formFieldChange";
export function init() {
return initAPI(InitActionList, HOME_INDEX, "get");
}
HomeReducer.js
import {
BillStatus,
CreditStatus,
InstallmentStatus,
} from "../../config/constant";
import { InitActionList } from "./HomeAction.js";
const initState = {
foo: 1,
bar: 10,
};
export default function (state = initState, action) {
const type = action.type;
const payload = action.payload;
const meta = action.meta;
switch (type) {
case InitActionList.start:
return state;
case InitActionList.success:
const currentStatus = getCurrentStatus(payload);
return {
...state,
foo: currentStatus,
};
case InitActionList.failure:
return state;
default:
return state;
}
}
Router.js
import { init as initHome } from "./homeAction";
export default function createRoutes(store) {
function initHome(store) {
return (nextState, replace, next) => {
// dispatch 页面加载的 Action,在数据加载完成后在执行 next() 以挂载组件
store.dispatch(homeInit()).then(() => next());
};
}
return {
component: App,
path: "/",
childRoutes: [
require("./activate"),
{
path: "test",
getComponent(nextState, cb) {
require.ensure(
[],
(require) => {
cb(null, require("../views/Test").default);
},
"Test",
);
},
},
],
indexRoute: {
getComponent(nextState, cb) {
require.ensure(
[],
(require) => {
cb(null, require("../views/Home").default);
},
"Home",
);
},
onEnter: initHome(store),
},
};
}
Index.js
import React from "react";
import { render } from "react-dom";
import { AppContainer } from "react-hot-loader";
import { Provider } from "react-redux";
import { hashHistory, Router } from "react-router";
import { syncHistoryWithStore } from "react-router-redux";
import "react-hot-loader/patch";
import createRoutes from "./routes";
import configureStore from "./store";
import "./style/app.scss";
export const store = configureStore(hashHistory, {});
const history = syncHistoryWithStore(hashHistory, store);
const root = document.getElementById("root");
const routers = createRoutes(store);
const renderRoot = () => {
render(
<AppContainer>
<Provider store={store} key="provider">
<Router routes={routers} history={history} key={Math.random()} />
</Provider>
</AppContainer>,
root,
);
};
if (module.hot) {
module.hot.accept("./routes", () => {
renderRoot();
});
}
renderRoot();
この記事は 2017年4月20日 に公開され、2017年4月20日 に最終更新されました。3090 日が経過しており、内容が古くなっている可能性があります。