Since the project uses React, React-Router, and Redux for development, it adopts a client-side rendering approach (this issue wouldn’t exist if SSR were used).
Problem
Therefore, each time a page is accessed, an initial request needs to be sent to fetch page data, and then the page is re-rendered. So, the previous flow from request to page load was like this:
Route match -> ComponentWillMount -> render -> ComponentDidMount -> dispatch(init())-> render -> componentDidUpdateFirst, route matching occurs. Then, the component is prepared for loading. It renders using the initial state from the Reducer. Then, the ComponentDidMount event is triggered. Within this event, an Action is dispatched to execute the page’s initialization request. Upon successful request, the component is re-rendered.
As you can see, displaying the final page requires the component to render twice. One render uses the initialState data hardcoded in the frontend reducer, and the other occurs after receiving data from the backend.
This sometimes leads to a flickering screen effect, resulting in a poor user experience.
Our requirement is that when entering a page, it should be rendered directly with the latest data fetched from the backend, and the entire page should only render once.
So, how can we solve this problem?
Solution
To solve this problem, it’s essential to load the data first, and then mount the page component. Therefore, the timing of data loading becomes crucial.
Drawing inspiration from traditional server-side rendering methods, the most suitable time for this is within the router.
Specifically for our project, this means making the page’s initial request within the onEnter event of react-router. Once all data requests are successful, the page is then loaded.
The entire page loading process then becomes like this:
Route match -> dispatch(init()) -> ComponentWillMount -> render -> ComponentDidMountSpecific Code Examples:
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();