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 .babelrc
or section babel
to filepackage.json
{
"presets": [
"react-app"
]
}
The preset babel-preset-react-app
is 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 initialError
will matter if getInitialProps
an 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 createRoutes
should 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 createStore
should 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 initialProps
from 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 scripts
file 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:3000
in 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 scripts
file 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 src
and 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:3000
in 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 .