🧊 Server Side Rendering and Create React App (aka React Scripts)

🧊 Server Side Rendering and Create React App (aka React Scripts)
Create React App and React Scripts projects are probably the most well-known way to quickly set up the React-based application. The motto of the project is 100% no configuration, everything is based on convention. This is a fairly robust solution that has everything you need except for the Server Side Rendering support.
As one of the ways to solve this, official documentation suggests to inject initial data or use the static snapshots. Unfortunately, both have drawbacks. The first will not allow search engines to properly analyze the static HTML, and the second does not pass down any state except what’s contained in the markup, so if you use Redux then you have to choose something else.
I have upgraded the example from my previous article to be used with Create React App. Now the package is called Create React Server and is now capable of launching the server-side rendered website like so:
create-react-server -r src/routes.js -s src/store.js
Copy
Bash
The authors of React Router determined that Google was indexing their sites well enough for our needs without server rendering, but one of the main reasons to do SSR is to deliver the content to users as fast as possible, sometimes it is even more important than tricking the Google bot.
Installation
First of all, let’s install the required packages:
npm install create-react-server --save-dev
Copy
Bash
Add .babelrc file or “babel” section to package.json
{ "presets": [ "react-app" ] }
Copy
JSON
The preset babel-preset-react-app is installed along with react-scripts, but for server rendering we need to refer to it directly.
React Router’s endpoint page
Just like before, the essence of Server Side Rendering is to determine which page has to be displayed based on router’s routes, figure out which data this page needs, load it, render HTML and send it along with the data to the client.
Server takes the endpoint page, calls its getInitialProps, inside of which we can dispatch some Redux actions and return the initial set of props (if Redux is not used). This method will be called on a client too (not if server has sent data though), which greatly simplifies the initial data load procedure.
// src/Page.jsimport React, {Component}from "react"; import {connect}from "react-redux"; import {withWrapper}from "create-react-server/wrapper"; import {withRouter}from "react-router";exportclassAppextendsComponent { staticasync getInitialProps({location, query, params, store}) { await store.dispatch(barAction()); return {custom: 'custom'}; }; render() { const {foo, bar, custom, initialError} =this.props; if (initialError)return ( <pre>Error {initialError.stack}</pre> ); return ( <div>Foo {foo}, Bar {bar}, Custom {custom}</div> ); }}// connect to Redux Provider as usual App = connect(state => ({foo: state.foo, bar: state.bar})(App);// connect to WrapperProvider, which has initialProps from server App = withWrapper(App);// add React Router just for sake of example App = withRouter(App);exportdefault App;
Copy
JavaScript
The prop initialError will have value if getInitialProps raised an exception.
The page that will be used as 404 stub should have 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 ( <div>404NotFound</div> ); }} export default withWrapper(NotFound);
Copy
JavaScript
Router
The function createRoutes should return routes for React Router, async routes are also supported, but for simplicity let’s skip them:
// src/routes.jsimport Reactfrom "react"; import {IndexRoute, Route} from "react-router"; import NotFound from './NotFound'; import App from './Page'; export default function (history) { return ( <Route path="/"> <IndexRoute component={App}/> <Route path='*' component={NotFound}/> </Router> ); }
Copy
JavaScript
Redux
The function createStore should take the initial state as an argument and return a new instance of 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 ); }
Copy
JavaScript
Additional parameters with NodeJS Request and Response will be added when function is used on a server, they can be used to create an initial state from scratch because first parameter will be undefined in this case.
Main app entry point
Now let’s put everything together:
// 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 = () => ( <Provider store={createStore(window.__INITIAL_STATE__)}> <WrapperProvider initialProps={window.__INITIAL__PROPS__}> <Router history={browserHistory}>{createRoutes()}</Router> </WrapperProvider> </Provider> ); render((<Root/>), document.getElementById('root'));
Copy
JavaScript
Start the server using simple CLI
We need to add a section scripts to package.json:
{ "build": "react-scripts build", "server": "create-react-server --createRoutes src/routes.js --createStore src/store.js" }
Copy
JSON
Now we can build & start the server:
npm run build npm run server
Copy
Bash
If we open http://localhost:3000 in browser we will see the page that has been rendered by the server. If you change the route then client side will take care of all data for the page.
In this mode the server-side build is not saved, it is dynamically created every time the server is started.
Start the server using API
If the CLI server is not flexible enough or if you decide to keep the server build then you can use the API.
In addition to previously installed packages let’s install babel-cli, it will be used to build the server:
npm install babel-cli --save-dev
Copy
Bash
Add following commands to scripts section ofpackage.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" }
Copy
JSON
The client part will still be built by Create React App (React Scripts), but the server with Babel, which will take everything from src and put the result in build-lib.
// src/server.jsimport 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 }));
Copy
JavaScript
Run:
npm run build npm run server
Copy
Bash
If we open http://localhost:3000 in browser we will see the same page that still has been rendered by the server.
More examples