javascriptreactjsreact-routerreact-reduxreact-ssr

Prevent browser from making the same async calls as the server


I am following this tutorial: https://crypt.codemancers.com/posts/2017-06-03-reactjs-server-side-rendering-with-router-v4-and-redux/ which i think is the 'standard' way of doing server side rendering in react (?).

Basically what happens is i use react router (v4) to make a tree of all the components that are about to get rendered:

const promises = branch.map(({ route }) => {
    return route.component.fetchInitialData
        ? route.component.fetchInitialData(store.dispatch)
        : Promise.resolve();
});

Wait for all those promises to resolve and then call renderToString.

In my components i have a static function called fetchInitialData which looks like this:

class Users extends React.Component {
    static fetchInitialData(dispatch) {
        return dispatch(getUsers());
    }
    componentDidMount() {
        this.props.getUsers();
    }
    render() {
        ...
    }
}

export default connect((state) => {
    return { users: state.users };
}, (dispatch) => {
    return bindActionCreators({ getUsers }, dispatch);
})(Users);

And all this works great except that getUsers is called both on the server and the client.

I could of course check if any users are loaded and not call getUsers in componentDidMount but there must be a better, explicit way to not make the async call twice.


Solution

  • After getting more and more familiar with react i feel fairly confident i have a solution.

    I pass a browserContext object along all rendered routes, much like staticContext on the server. In the browserContext i set two values; isFirstRender and usingDevServer. isFirstRender is only true while the app is rendered for the first time and usingDevServer is only true when using the webpack-dev-server.

    const store = createStore(reducers, initialReduxState, middleware);

    The entry file for the browser side:

    const browserContext = {
        isFirstRender: true,
        usingDevServer: !!process.env.USING_DEV_SERVER
    };
    
    const BrowserApp = () => {
        return (
            <Provider store={store}>
                <BrowserRouter>
                    {renderRoutes(routes, { store, browserContext })}
                </BrowserRouter>
            </Provider>
        );
    };
    
    hydrate(
        <BrowserApp />,
        document.getElementById('root')
    );
    
    browserContext.isFirstRender = false;
    

    USING_DEV_SERVER is defined in the webpack config file using webpack.DefinePlugin

    Then i wrote a HOC component that uses this information to fetch initial data only in situations where it is needed:

    function wrapInitialDataComponent(Component) {
        class InitialDatacomponent extends React.Component {
            componentDidMount() {
                const { store, browserContext, match } = this.props;
    
                const fetchRequired = browserContext.usingDevServer || !browserContext.isFirstRender;
    
                if (fetchRequired && Component.fetchInitialData) {
                    Component.fetchInitialData(store.dispatch, match);
                }
            }
            render() {
                return <Component {...this.props} />;
            }
        }
    
        // Copy any static methods.
        hoistNonReactStatics(InitialDatacomponent, Component);
    
        // Set display name for debugging.
        InitialDatacomponent.displayName = `InitialDatacomponent(${getDisplayName(Component)})`;
    
        return InitialDatacomponent;
    }
    

    And then the last thing to do is wrap any components rendered with react router with this HOC component. I did this by simply iterating over the routes recursively:

    function wrapRoutes(routes) {
        routes.forEach((route) => {
            route.component = wrapInitialDataComponent(route.component);
    
            if (route.routes) {
                wrapRoutes(route.routes);
            }
        });
    }
    
    const routes = [ ... ];
    
    wrapRoutes(routes);
    

    And that seems to do the trick :)