Photo by Stage 7 Photography

React Render server

Here are some of the pros and cons of server-side rendering that are already well known. The purpose of this article is to show you how to upgrade a simple browser-side rendering React SPA step by step to support server-side rendering.

Initialize a normal single page application (browser-side rendering)

Before building the server side rendering application, we now build a single page application based on the browser side rendering. The single page application contains simple routing functions.

mkdir react-ssr
cd react-ssr
yarn init
Copy the code

Dependent installation:

yarn add react react-dom react-router-dom
Copy the code

First create the entry file SRC/app.jsx for your App:

import React from 'React';
import { Switch, Route, Link } from 'react-router-dom';

import Home from './pages/Home';
import Post from './pages/Post';

export default () => (
    <div>
        <Switch>
            <Route exact path="/" component={ Home } />
            <Route exact path="/post" component={ Post } />
        </Switch>
    </div>
)
Copy the code

SRC /pages/ home.jsx; SRC /pages/ post.jsx; SRC /pages/ post.jsx;

// Home.jsx
import React from 'react';
import { Link } from 'react-router-dom';

export default() = > (<div>
        <h1>Page Home.</h1>
        <Link to="/post">Link to Post</Link>
    </div>
);

// Post.jsx
import React, { Component } from 'react';
import { Link } from 'react-router-dom';

export default class Post extends Component {
    constructor(props) {
        super(props);
        this.state = {
            post: {}}; } componentDidMount() { setTimeout((a)= > this.setState({
            post: {
                title: 'This is title.'.content: 'This is content.'.author: 'Big chestnut.'.url: 'https://github.com/justclear',}}),2000);
    }
    render() {
        const post = this.state.post;
        return (
            <div>
                <h1>Page Post</h1>
                <Link to="/">Link to Home</Link>
                <h2>{ post.title }</h2>
                <p>By: { post.by }</p>
                <p>Link: <a href={post.url} target="_blank">{post.url}</a></p>
            </div>); }};Copy the code

Then create the entry file SRC /index.jsx for webPack:

import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter } from 'react-router-dom';
import App from './App';

ReactDOM.render(
    <BrowserRouter>
        <App></App>
    </BrowserRouter>
    , document.getElementById('root'));
Copy the code

Package. Json:

{
    "scripts": {
        "build:client": "NODE_ENV=development webpack -w",}}Copy the code

React redirects a single page application.

Added server-side rendering

As the name implies, you must have a server in order to add server-side rendering, so here’s the Express framework for convenience (you can also use koa, Fastify, Restify, and all the other familiar frameworks) :

yarn add express
Copy the code

Create the server code entry file server/index.js:

import fs from 'fs';
import path from 'path';
import express from 'express';

import React from 'react';
import { StaticRouter } from "react-router-dom";
import { renderToString } from 'react-dom/server';
import App from '.. /src/App';

const app = express();

app.get('/ *', (req, res) => {
    const renderedString = renderToString(
        <StaticRouter>
            <App></App>
        </StaticRouter>
    );

    fs.readFile(path.resolve('index.html'), 'utf8', (error, data) => {
        if (error) {
            res.send(`<p>Server Error</p>`);
            return false;
        }

        res.send(data.replace('<div id="root"></div>'.`<div id="root">${renderedString}</div>`)); })}); app.listen(3000);
Copy the code

Configure webpack.server.js:

const path = require('path');

module.exports = {
    mode: 'development'.entry: './server/index.js'.output: {
        filename: 'app.js'.path: path.resolve('server/build'),},target: 'node'.resolve: {
        extensions: ['.js'.'.jsx'.'.ts'.'.tsx'.'.json'],},module: {
        rules: [{
            test: /\.jsx? $/.use: 'babel-loader'.exclude: /node_modules/,}].}};Copy the code

Package. Json:

{
    "scripts": {
        "build:server": "NODE_ENV=development webpack -w --config webpack.server.js"."start": "nodemon server/build/app.js"}},Copy the code

Note: For server render, the documentation suggests replacing reactdom. render in SRC /index.jsx with reactdom.hydrate as server render will no longer be supported in the next major release.

react-dom docs: Using ReactDOM.render() to hydrate a server-rendered container is deprecated and will be removed in React 17. Use hydrate() instead.

Finally, after NPM start, you will see the following page:

At first glance it looks the same as the browser-side rendering, but if we look at the source code for the two pages separately, we can see the difference:

It is obvious that the

in the source code of the second server rendered page has some extra code. If you look closely, you will see that it is actually the code rendered by home.jsx.

At this point, we have implemented the React server rendering functionality.

However, if you click on the Link to Post Link in the page, you will see that home.jsx is still rendered after the route jump/Post because we did not match the route on the server.

The server matches the route

The react-router-dom routing module provides a matchPath method to match routes.

SRC /routes.js SRC /routes.js SRC /routes.js

// routes.js
import Home from './pages/Home';
import Post from './pages/Post';

export default [{
    path: '/'.exact: true.component: Home
}, {
    path: '/post'.exact: true.component: Post,
}];

Copy the code

Then in server/index.js we introduce:

// ...
import { StaticRouter, matchPath } from 'react-router-dom';
import routes from '.. /src/routes';
// ...

app.get('/ *', (req, res) => {
    const currentRoute = routes.find(route= > matchPath(req.url, route)) || {};
    // ...
    const renderedString = renderToString(
        <StaticRouter location={ req.url} >
            <App></App>
        </StaticRouter>
    );
});
Copy the code

Match the information of the current route through the find method of the array with the matchPath method, and then add the attribute of location to the
component and pass the current route req.url. If you re-click the Link to Post Link in the page, the components under the/Post route will render normally:

Now, you might notice that when you jump to the Post page, you don’t get the asynchronous data defined in componentDidMount. This is because componentDidMount only executes in the browser. Therefore, the server will not execute this function, so it will not get the data, which is obviously not the desired result. The expectation is that after a route jump, the asynchronous data can be retrieved as normal as the browser-side rendering.

So how do we get this data on the server and then send it back to the browser?

The server asynchronously obtains data

Add a new SRC /helpers/ fetchdata.js helper to retrieve data:

export default() = > {return new Promise((resolve) = > {
        setTimeout((a)= > resolve({
            title: 'This is title.'.content: 'This is content.'.author: 'Big chestnut.'.url: 'https://github.com/justclear',}).2000); })};Copy the code

The implementation idea is to determine whether the components contained in the current route need to load data when matching the route. If so, load:

// ...
app.get('/ *', (req, res) => {
    const currentRoute = routes.find(route= > matchPath(req.url, route)) || {};
    const promise = currentRoute.fetchData ? currentRoute.fetchData() : Promise.resolve(null);

    promise.then(data= > {
        // data here...
    }).catch(console.log);
});
Copy the code

The logic here is to determine whether the fetchData key in the SRC /routes.js route object has a value. If fetchData is judged to be true by the tri operation, it is considered that the route needs to obtain data. FetchData = ‘post’; / / fetchData = ‘post’;

// src/routes.js
import Home from './pages/Home';
import Post from './pages/Post';

import fetchData from './helpers/fetchData';

export default [{
    path: '/'.exact: true.component: Home
}, {
    path: '/post'.exact: true.component: Post,
    fetchData,
}];
Copy the code

Currentroute.fetchdata () promises to render the POST component when the route matches/POST:

promise.then(data= > {
    const context = {
        data,
    };
    const renderedString = renderToString(
        <StaticRouter context={context} location={req.url}>
            <App></App>
        </StaticRouter>
    );

    res.send(template());

    function template() {
        return ` <! DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, Initial scale=1.0"> <meta http-equiv=" x-UA-compatible "content="ie=edge"> <title>React Server Side Rendering</title> </head> <body> <div id="root">${renderedString}</div>
                <script>window.__ROUTE_DATA__ = The ${JSON.stringify(data)}</script>
                <script src="dist/app.js"></script>
            </body>
            </html>

        `;
    }
}).catch(console.log);
Copy the code

After getting the data, you should pass it to the context property of the
component so that you can get the data from the props. StaticContext of the component itself. In addition, you need to assign json.stringify (data) to window.__route_data__, __ROUTE_DATA__ can be named however you want, __ROUTE_DATA__ is used to determine the value of window.__route_data__ within the component to take different strategies for retrieving data.

However, if you click Link to Post, you may find that the page cannot be opened:

This is because the request /dist/app.js is treated as a normal route and is not treated as a static resource to return valid JavaScript code. The solution is to add the following code to server/index.js:

// ...
const app = express();
app.use(express.static('dist'));
// ...
Copy the code

Dist /app.js”>

Now /app.js returns the JavaScript code correctly.

Now that the server has returned the data retrieved to the browser as window.__route_data__ = json.stringify (data), we need to use this state inside the post.jsx component:

// ...
export default class Post extends Component {
    constructor(props) {
        super(props);
        if (props.staticContext && props.staticContext.data) {
            this.state = {
                post: props.staticContext.data
            };
        } else {
            this.state = {
                post: {}}; } } componentDidMount() {if (window.__ROUTE_DATA__) {
            this.setState({
                post: window.__ROUTE_DATA__,
            });
            delete window.__ROUTE_DATA__;
        } else {
            fetchData().then(data= > {
                this.setState({
                    post: data, }); }}})// ...
};
Copy the code

__ROUTE_DATA__ is null, so fetchData is executed to retrieve the data. So you’ll see a 2 second wait after entering/POST before the data is displayed. If you refresh the page, you don’t have to wait to see the results.

conclusion

React server rendering support is now almost complete, but it’s far from enough. It would be a lot more complicated to use in real projects. Optimize your code with tools like Webpack Dynamic Imports and React-Loadable and how it works with Redux.

The purpose of this article is to give some students who are not familiar with React Server Side Rendering technology or have no concept of how to render servers.

To see the full project, go to Github.