原文 : Demystifying server-side rendering in React

By Alex Moldovan

React server rendering

Let’s take a closer look at React Server Side Rendering, a feature that allows you to build universal applications using React.

Server-side rendering (SSR) is the process of rendering a website built from a front-end framework in the form of a back-end rendering template.

Applications that can be rendered on both the server and the client are called universal applications.

Why SSR

To understand why we need SSR, we first need to understand the evolution of Web applications over the last 10 years.

This is closely related to the rise of single page apps (SPA). Compared with traditional SSR applications, SPA has great advantages in speed and user experience.

But there’s a problem. The initial server request for a SPA usually returns an HTML file with no DOM structure, just a bunch of CSS and JS links. The application then needs to fetch some additional data to render the associated HTML tags.

This means that users will have to wait longer for the initial rendering. This also means that a crawler might parse your page empty.

Therefore, the solution to this problem is to render your app on the server first (render the first screen) and then use SPA on the client.

SSR + SPA = Universal App

You’ll find the word Isomorphic App in other articles, which is the same thing as Universal App.

Now, the user doesn’t have to wait for your JS to load, and can retrieve fully rendered HTML immediately after the initial request returns a response.

Imagine the speed boost this could bring to users on slow 3G networks. Instead of waiting 20 seconds for the site to load, you can get content on the screen almost immediately.

All requests to your server now return fully rendered HTML. Great news for your SEO department! A web crawler will index whatever content you present on your server, just as it does for other static sites on the web.

To review, SSR has the following two benefits:

  1. Improved first screen rendering time
  2. Complete indexable HTML pages (good for SEO)

Understand SSR step by step

Let’s take a step by step iterative approach to build a complete SSR instance. We’ll start with the React server rendering API and work our way up.

You can understand each build step by following the repository and looking at the tags defined there.

Basic setup

First, in order to use SSR, we need a server. We’ll use a simple Express service to render our React application.

server.js:

import express from "express";
import path from "path";

import React from "react";
import { renderToString } from "react-dom/server";
import Layout from "./components/Layout";

const app = express();

app.use( express.static( path.resolve( __dirname, ".. /dist"))); app.get("/ *", ( req, res ) => {
    const jsx = ( <Layout /> );
    const reactDom = renderToString( jsx );

    res.writeHead( 200, { "Content-Type": "text/html" } );
    res.end( htmlTemplate( reactDom ) );
} );

app.listen( 2048 );

function htmlTemplate( reactDom ) {
    return `
        <!DOCTYPE html>
        <html>
        <head>
            <meta charset="utf-8">
            <title>React SSR</title>
        </head>
        
        <body>
            <div id="app">${ reactDom }</div>
            <script src="./app.bundle.js"></script>
        </body>
        </html>
    `;
}
Copy the code

In line 10, we specify the folder where Express needs to serve the static files.

We created a route to handle all non-static requests. The route returns a rendered HTML string.

Note that we use the same Babel plug-in for both client and server code, so JSX and ES6 Modules work in server.js.

The corresponding render function on the client side is reactdom.hydrate. This function will receive the React App rendered by the server and attach event handlers.

To see a complete example, check out basicTag in the repository.

All right! You just created your first server-side render of the React app!

React Router

We have to be honest, the app doesn’t have many features yet. So let’s add a few more routes and think about how we can handle this part on the server side.

/ components/Layout. Js:

import { Link, Switch, Route } from "react-router-dom";
import Home from "./Home";
import About from "./About";
import Contact from "./Contact";

export default class Layout extends React.Component {
    / *... * /

    render() {
        return( <div> <h1>{ this.state.title }</h1> <div> <Link to="/">Home</Link> <Link to="/about">About</Link> <Link to="/contact">Contact</Link> </div> <Switch> <Route path="/" exact component={ Home } /> <Route path="/about" exact component={ About } /> <Route path="/contact" exact component={ Contact } /> </Switch> </div> ); }}Copy the code

The Layout component now renders multiple routes on the client side.

We need to simulate the routing on the server. You can see the changes that should be made below.

Server. Js:

/ *... * /
import { StaticRouter } from "react-router-dom";
/ *... * /

app.get( "/ *", ( req, res ) => {
    const context = { };
    const jsx = (
        <StaticRouter context={ context } location={ req.url} >
            <Layout />
        </StaticRouter>
    );
    const reactDom = renderToString( jsx );

    res.writeHead( 200, { "Content-Type": "text/html"}); res.end( htmlTemplate( reactDom ) ); });/ *... * /
Copy the code

On the server side, we need to outsource our React application to a layer of StaticRouter and give it a location.

Note: Context is used to track potential redirects when rendering the React DOM. This needs to be handled by a response from the server to 3XX.

You can see a complete example of routing in the Router TAB of the same repository.

Redux

Now that we have the routing capability, let’s integrate Redux.

In a simple scenario, we handle client-side state management through Redux. But what if we need to render part of the DOM based on state? At this point, it is necessary to initialize Redux on the server side.

If your app dispatches actions on the server, it needs to capture the state and send it to the client over the network along with the HTML results. On the client side, we load this initial state into Redux.

First let’s look at the server-side code:


/ *... * /
import { Provider as ReduxProvider } from "react-redux";
/ *... * /

app.get( "/ *", ( req, res ) => {
    const context = { };
    const store = createStore( );

    store.dispatch( initializeSession( ) );

    const jsx = (
        <ReduxProvider store={ store} >
            <StaticRouter context={ context } location={ req.url} >
                <Layout />
            </StaticRouter>
        </ReduxProvider>
    );
    const reactDom = renderToString( jsx );

    const reduxState = store.getState( );

    res.writeHead( 200, { "Content-Type": "text/html"}); res.end( htmlTemplate( reactDom, reduxState ) ); }); app.listen(2048 );

function htmlTemplate( reactDom, reduxState ) {
    return `
        /* ... */
        
        <div id="app">${ reactDom }</div>
        <script>
            window.REDUX_DATA = The ${JSON.stringify( reduxState ) }</script> <script src="./app.bundle.js"></script> /* ... * / `;
}
Copy the code

It looks ugly, but we need to send the full JSON-formatted state to the client along with our HTML.

Then let’s look at the client side:

app.js

import React from "react";
import ReactDOM from "react-dom";
import { BrowserRouter as Router } from "react-router-dom";
import { Provider as ReduxProvider } from "react-redux";

import Layout from "./components/Layout";
import createStore from "./store";

const store = createStore( window.REDUX_DATA );

const jsx = (
    <ReduxProvider store={ store} >
        <Router>
            <Layout />
        </Router>
    </ReduxProvider>
);

const app = document.getElementById( "app" );
ReactDOM.hydrate( jsx, app );
Copy the code

Note that we called createStore twice, first on the server side and then on the client side. However, on the client side we initialize the state on the client side using any state saved on the server side. The process is similar to DOM Chocolate.

You can see a complete example of REdux in the Redux TAB in the same repository.

Fetch Data

The last trickier challenge is loading the data. Suppose we have an API that provides JSON data.

In our code repository, I retrieved all the events of the 2018 F1 season from a public API. Suppose we want to display all times on the home page.

We can call our React API from the client after the React app has mounted and rendered everything. However, this can lead to a poor user experience and may require showing a Loader or spinner before the user can see the relevant content.

In our SSR app, Redux first stores data on the server and then sends the data to the client. We can use that to our advantage.

What if we made an API call on the server, stored the result in Redux, and rendered it to the client using a full HTML rendering with the relevant data?

But how can we tell which page an API call corresponds to?

First, we need a different way to declare routes. Let’s create a routing configuration file.

export default[{path: "/".component: Home,
        exact: true}, {path: "/about".component: About,
        exact: true}, {path: "/contact".component: Contact,
        exact: true}, {path: "/secret".component: Secret,
        exact: true,},];Copy the code

Then we statically declare the data requirements for each component:

/ *... * /
import { fetchData } from ".. /store";

class Home extends React.Component {
    / *... * /

    render( ) {
        const { circuits } = this.props;

        return (
            / *... * /
        );
    }
}
Home.serverFetch = fetchData; // static declaration of data requirements

/ *... * /
Copy the code

Remember that serverFetch is freely named.

Note that fetchData is a Redux thunk action that returns a Promise when it is dispatched.

On the server side, we can use a function called matchPath from the React-Router.

/ *... * /
import { StaticRouter, matchPath } from "react-router-dom";
import routes from "./routes";

/ *... * /

app.get( "/ *", ( req, res ) => {
    / *... * /

    const dataRequirements =
        routes
            .filter( route= > matchPath( req.url, route ) ) // filter matching paths
            .map( route= > route.component ) // map to components
            .filter( comp= > comp.serverFetch ) // check if components have data requirement
            .map( comp= > store.dispatch( comp.serverFetch( ) ) ); // dispatch data requirement

    Promise.all( dataRequirements ).then( ( )= > {
        const jsx = (
            <ReduxProvider store={ store} >
                <StaticRouter context={ context } location={ req.url} >
                    <Layout />
                </StaticRouter>
            </ReduxProvider>
        );
        const reactDom = renderToString( jsx );

        const reduxState = store.getState( );

        res.writeHead( 200, { "Content-Type": "text/html"}); res.end( htmlTemplate( reactDom, reduxState ) ); }); });/ *... * /
Copy the code

In this way, we get a list of components that will mount when React starts rendering as a string under the current URL.

We collected the Data requirements and waited for all API calls to return the data. Finally, we move on to server-side rendering, where the data is available in Redux.

You can see a complete example of data fetching in the fetch-data tag in the same repository.

You may notice that this incurs a performance penalty because we delay rendering until after the data has been fetched.

It’s up to you to make your own trade-offs, and you need to try to figure out which calls are important and which are not. For example, in an e-commerce app, fetch product list is important, and product price and filters in sidebar can be lazy loaded.

Helmet

Let’s look at SEO as one of the benefits of SSR. When using React, you might want to set different title, meta Tags, keywords, and so on in the tag.

Remember that the tag is usually not part of the React app.

React-helmet provides a good solution in this case. And it has good support for SSR.

import React from "react";
import Helmet from "react-helmet";

const Contact = (a)= > (
    <div>
        <h2>This is the contact page</h2>
        <Helmet>
            <title>Contact Page</title>
            <meta name="description" content="This is a proof of concept for React SSR" />
        </Helmet>
    </div>
);

export default Contact;
Copy the code

You simply add your head data anywhere in the component tree. This allows you to change values outside of the mounted React App on the client.

Now, let’s add support for SSR:

/ *... * /
import Helmet from "react-helmet";
/ *... * /

app.get( "/ *", ( req, res ) => {
    / *... * /
        const jsx = (
            <ReduxProvider store={ store} >
                <StaticRouter context={ context } location={ req.url} >
                    <Layout />
                </StaticRouter>
            </ReduxProvider>
        );
        const reactDom = renderToString( jsx );
        const reduxState = store.getState( );
        const helmetData = Helmet.renderStatic( );

        res.writeHead( 200, { "Content-Type": "text/html"}); res.end( htmlTemplate( reactDom, reduxState, helmetData ) ); }); }); app.listen(2048 );

function htmlTemplate( reactDom, reduxState, helmetData ) {
    return ` <! DOCTYPE html> <html> <head> <meta charset="utf-8">${ helmetData.title.toString( ) }
            ${ helmetData.meta.toString( ) }<title>React SSR</title> </head> /* ... * / `;
}
Copy the code

We now have a fully functional example of React SSR.

We started by rendering a simple HTML string through Express and gradually added routing, state management, and data retrieval. Finally, we make changes outside of the React application scope (handling the head tag)

Complete example, please look at https://github.com/alexnm/react-ssr.

summary

As you can see, SSR is not a big problem either. But it can get complicated. If you build your requirements step by step, it will be easier to master.

Is SSR worth using in your app? As always, it depends. This is a must if your site is targeting tens of thousands of users. If you’re building an application like a tool/dashboard, you probably don’t need it.

Of course, taking advantage of universal Apps can really improve the front-end community.

Do you have a similar approach to SSR? Or do you think I’m missing something in this article? Send me a message on Twitter.

If you find this article useful, please help me share it in the community.