Create React App (aka React Scripts) and server rendering with Redux and Router

    From the comments on the article, it became clear that so many people are leaning towards the ecosystem of the Create React App (aka React Scripts). This is quite reasonable since this is the most popular and easy-to-use product (due to the lack of configuration and support from leading people of the React community), which, in addition, has almost everything you need - assembly, development mode, tests, coverage statistics. What is missing is server-side rendering.


    As one of the ways in the official documentation, it is proposed to either drive the initial data into the template or use static casts . The first approach does not allow search engines to index static HTML normally, and the second does not support forwarding any initial data except HTML ( phrase from the documentation : this doesn't pass down any state except what's contained in the markup). Therefore, if Redux is used, then you will have to use something else for rendering.


    I adapted the example from the article for use with the Create React App, now it is called Create React Server and can start server-side rendering with the command:


    create-react-server --createRoutes src/routes.js --createStore src/store.js

    With this launch, no special configuration is required, everything is done through command-line options. If necessary, you can slip your own templates and handlers in the same way.


    A slight lyrical digression. As the authors of React Router say , their sites are indexed by Google without problems and without any server rendering. Maybe so. But one of the main problems is not only Google, but also the fast delivery of content to the user, and this can even be more important than indexation, which can be fooled.


    Installation


    First, install the packages required for this example:


    npm install create-react-server --save-dev

    Add file .babelrcor section babelto filepackage.json


    {
        "presets": [
          "react-app"
        ]
    }

    The preset babel-preset-react-appis placed with react-scripts, but for server rendering we need to explicitly refer to it.


    Page (i.e. React Router Endpoint)


    As before, the essence of server rendering is quite simple: on the server we need to determine, based on the rules of the router, which component will be shown on the page, find out what data it needs to work, request this data, render HTML, and send this HTML along with the data per client.


    The server takes the final component, calls it getInitialProps, inside which you can make a dispatch of Redux actions and return the initial set props(in case Redux is not used). The method is called both on the client and on the server, which greatly simplifies the initial data loading.


    // src/Page.js
    import React, {Component} from "react";
    import {connect} from "react-redux";
    import {withWrapper} from "create-react-server/wrapper";
    import {withRouter} from "react-router";
    export class App extends Component {
        static async getInitialProps({location, query, params, store}) {
            await store.dispatch(barAction());
            return {custom: 'custom'}; // это станет начальным набором props при рендеринге
        };
        render() {
            const {foo, bar, custom, initialError} = this.props;
            if (initialError) return (
    Ошибка в функции getInitialProps: {initialError.stack}
    ); return (
    Foo {foo}, Bar {bar}, Custom {custom}
    ); } } // подключаемся к Redux Provider как обычно App = connect(state => ({foo: state.foo, bar: state.bar})(App); // подключаемся к WrapperProvider, который тянет initialProps с сервера App = withWrapper(App); // до кучи подключаемся к React Router App = withRouter(App); export default App;

    The variable initialErrorwill matter if getInitialPropsan error occurs in the function , and it does not matter where - on the client or on the server, the behavior is the same.


    The page to be used as a stub for 404 errors should have a static property notFound:


    // src/NotFound.js
    import React, {Component} from "react";
    import {withWrapper} from "create-react-server/wrapper";
    class NotFound extends Component {
        static notFound = true;
        render() {
            return (
                
    404 Not Found
    ); } } export default withWrapper(NotFound);

    Router


    The function createRoutesshould return the rules of the router, asynchronous routes are also supported, but for simplicity we omit this for now:


    // src/routes.js
    import React from "react";
    import {IndexRoute, Route} from "react-router";
    import NotFound from './NotFound';
    import App from './Page';
    export default function(history) {
        return 
        ;
    }

    Redux


    The function createStoreshould take the initial state as a parameter and return a new one Store:


    // src/store.js
    import {createStore} from "redux";
    function reducer(state, action) { return state; }
    export default function (initialState, {req, res}) {
        if (req) initialState = {foo: req.url};
        return createStore(
            reducer,
            initialState
        );
    }

    When the function is called on the server, the second parameter will have the Request and Response objects from NodeJS, you can extract some information and put it in the initial state.


    Main entry point


    Let's put everything together, and also add a special wrapper to receive initialPropsfrom the server:


    // src/index.js
    import React from "react";
    import {render} from "react-dom";
    import {Provider} from "react-redux";
    import {browserHistory, match, Router} from "react-router";
    import {WrapperProvider} from "react-router-redux-middleware/wrapper";
    import createRoutes from "./routes";
    import createStore from "./store";
    const Root = () => (
        {createRoutes()}
    );
    render((), document.getElementById('root'));

    Starting a simple server through the console utility


    Add scripts to the scriptsfile section package.json:


    {
        "build": "react-scripts build",
        "server": "create-react-server --createRoutes src/routes.js --createStore src/store.js
    }

    And run


    npm run build
    npm run server

    Now if we open http://localhost:3000in the browser, we will see a page prepared on the server.


    In this mode, the result of the server assembly is not stored anywhere and is calculated on the fly every time.


    Starting the server through the API and saving the assembly results


    If the command line capabilities have become scarce, or you want to store the results of a server assembly, you can always create a server not through the CLI, but through the API.


    Install in addition to the previous packages babel-cli, you will need it to build the server:


    npm install babel-cli --save-dev

    Add scripts to the scriptsfile section package.json:


    {
        "build": "react-scripts build && npm run build-server",
        "build-server": "NODE_ENV=production babel --source-maps --out-dir build-lib src",
        "server": "node ./build-lib/server.js"
    }

    Thus, the client part will still be collected by Create React App (React Scripts), and the server part will be collected using Babel, which will take everything srcand put it in build-lib.


    // src/server.js
    import path from "path";
    import express from "express";
    import {createExpressServer} from "create-react-server";
    import createRoutes from "./createRoutes";
    import createStore from "./createStore";
    createExpressServer({
        createRoutes: () => (createRoutes()),
        createStore: ({req, res}) => (createStore({})),
        outputPath: path.join(process.cwd(), 'build'),
        port: process.env.PORT || 3000
    }));

    Run:


    npm run build
    npm run server

    Now if we open it http://localhost:3000in the browser again, then we will again see the same page prepared on the server.


    The full example code can be found here: https://github.com/kirill-konshin/react-router-redux-middleware/tree/master/examples/create-react-app .


    Also popular now: