Universal create-react-app, step by step

This post will explain to you how to refactor an app created with create-react-app step by step in order to make it server-side rendering capable (AKA universal).

In the link below you can view and execute the final refactoring https://github.com/leanjscom/universal-create-react-app

Development

Production

Before moving things around and changing code in create-react-app, let’s explain a few things.

create-react-app uses a WebpackDevServer to build and serve you the app in development environment. WebpackDevServer basically does two things 1) it builds your app (transpiles JS, bundles assets, etc) using Webpack, and 2) it starts a server to serve you the static assets such as the index.html page and the bundle.js. That index.html will contain the script tag with the JS of your app, something like:

<script type=”application/javascript” src=”/static/js/bundle.js”></script>

The script is added dynamically by Webpack so you won’t see the script tag in /public/index.html.

As the name suggests, we use WebpackDevServer in the “Dev” environment. In production, we build an optimized JS file that can be served from a “source”, this could be our server, a CDN, etc. We don’t care because our app is a static JavaScript bundle. In a client-side only app we just need to set up the server during development.

In a universal app we want to render our React components on the server-side separate from the client. Therefore, we’ll need a server in both the development and production environments to render on the server-side.

Let’s do it! First, create an app using `create-react-app my-app`.

If you want to see your app running, execute `yarn start`. That will run the following script: scripts/start.js. What that script does and we want to keep in our universal app is:

1 - Find an available port to start the WebpackDevServer (3000 by default)

2 - Get the Webpack configuration for the development environment.

3 - Create a WebpackDevServer with a custom Webpack compiler (to configure custom messages among other things)

4 - Start WebpackDevServer on the available port.

Now we are going to refactor the app created by create-react-app to make it server-side (universal).

Step 1. Restructuring folders

Execute in your terminal `npm run eject`. Since we are going to change the scripts and configuration of the build, we need to run eject.

Now we are going to move some files. First, create the following folders in src/

src/client
src/server
src/shared

/client

Move:
src/index.js to src/client/index.js
src/index.css to src/client/index.css
src/registerServiceWorker.js to src/client/registerServiceWorker.js

Copy the content of src/App.css to src/client/index.css. The reason is we are not going to execute babel style-loader on the server-side since at this point in time it’s not compatible with universal (it requires a window object)

Next edit src/client/index.js:
- Replace import App from ‘./App’; for import App from ‘../shared/App’;
- Add import { BrowserRouter as Router } from ‘react-router-dom’
- Replace 
ReactDOM.render(<App />, document.getElementById(‘root’));
with:
ReactDOM.render(
 <Router><App /></Router>,
 document.getElementById(‘root’)
);

Yes we need to add a package, so let’s execute in our terminal:
`yarn add react-router-dom`

/shared

Please move:
- src/App.js to src/shared/App.js
- src/App.test.js to src/shared/App.test.js
- src/logo.svg to src/shared/logo.svg

/server

Let’s install the dependencies we need: `yarn add express nodemon webpack-node-externals http-proxy-middleware isomorphic-fetch`

In the src/server we are going to create 3 files:
- src/server/index.js
- src/server/app.js
- src/server/render.js

Step 2. Implementing the server-side

/src/server/index.js

Let’s implement first src/server/index.js. Responsibilities of this file are:

1. Create and start the server, Express in this example.
import express from ‘express’
//…
const app = express()

2. If it is production, map the url path ‘/static’ with the directory /bundle/client/static. So in production we serve the production build

If it is development then we have to proxy the url path ‘/static’ with the url where WebpackDevServer is running. We also need to enable web sockets (ws: true). Finally we need to redirect the path ‘/sockjs-node’ to WebpackDevServer. So HMR (https://webpack.github.io/docs/hot-module-replacement.html) will still work. This way, in development, every request to your Express server that has to do with your bundle will be managed by WebpackDevServer

3. Map the build assets with the root url path

app.use(‘/’, express.static(‘build/client’))

4. Use the code that has to do with your React app

import reactApp from ‘./app’
//…
app.use(reactApp)

Note the order of the above 4 points is important. You can see a full implementation of src/server/index.js here https://github.com/leanjscom/universal-create-react-app/blob/master/src/server/index.js

/src/server/app.js

Let’s have a look to src/server/app.js. This server/app.js is going to be an Express middleware, so it has to be a function with the following parameters: const reactApp = (req, res) => { }

Responsibilities of that file are:
1. Render the HTML of that url into the response
2. Set the right http status

react-router v4 is very nicely designed with React’s “way of thinking”, so we don’t have to do anything like in previous versions to match the url and the component at this level. We can get the HTML for that req.url just by doing:

HTML = render(
 <Router context={{}} location={req.url}>
 <App /> 
 </Router>)

But we are going to add a little trick. We want to return a status 404 if the page is not found. Since the match is done by react-router down in the tree, we can’t know if that req.url is a match or not without adding some custom matching. Here you have an example of what I mean https://github.com/technology-ebay-de/universal-react-router4/blob/master/src/server/index.js (more on that https://ebaytech.berlin/universal-web-apps-with-react-router-4-15002bb30ccb.)

So what we are going to do instead is we are going to let React tell us if there was a match for a “not found page”. To do that we are going to set in the context the following function:

const setStatus = newStatus => { status = newStatus }

So the “Not found component” that is rendered when there is no match should get that function from the context and set the status to 404. https://github.com/leanjscom/universal-create-react-app/blob/master/src/shared/App.js#L19. This way we don’t have to define routes in two different places, and match them twice.

We add the setStatus function to the context by using this simple and generic context provider https://github.com/leanjscom/react-context-component

HTML = render(
 <Context setStatus={setStatus}>
 <Router context={{}} location={req.url}><App /></Router> 
 </Context>
)

You can see a full implementation of src/server/app.js here https://github.com/leanjscom/universal-create-react-app/blob/master/src/server/app.js

/src/server/render.js

Responsibilities of this file:
1- Contains an HTML template for our pages
2- renderToString our React app
3- Sets the url of the statics: main.css and bundle.js

Note, in the 3rd step if we are in development environment we don’t want to set any CSS because that’s Webpack HMR job.

You can see an implementation here https://github.com/leanjscom/universal-create-react-app/blob/master/src/server/render.js

Step 3. Scripts.

/package.json

We are going to add the following scripts:
“serve”: “NODE_ENV=production node ./build/server/bundle.js”,
“build-client”: “node scripts/build-client.js”,
“build-server”: “node scripts/build-server.js”,
“build”: “npm run build-client && npm run build-server”,

/scripts

Rename scripts/build.js to build-client.js and change:
const config = require(‘../config/webpack.config.prod’);
to 
const config = require(‘../config/webpack.config.client.prod’)

Find this line:
if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) {
and remove paths.appHtml. The reason is we don’t serve that html page anymore, it’s our Express server who takes care of that now.

/scripts/build-server.js

Responsabilities of this file /scripts/build-server.js:

1. Read the asset manifest and set it in process.env.REACT_APP_ASSET_MANIFEST so we can use it in our src/server/render.js

Note, REACT_APP prefix is important otherwise Webpack won’t include it in the transpiled code.

2. Clean the build/server directory fs.emptyDirSync(paths.serverBuild);

3. Compile the code using compiler = webpack(config); // notice that config is const config = require(‘../config/webpack.config.server’);

You can see an implementation of build-server.js here https://github.com/leanjscom/universal-create-react-app/blob/master/scripts/build-server.js

/scripts/start.js

This script will start both the client(WebpackDevServer) and the server(Express compiled with Webpack). We have to change a few things here:

1. Before requiring ‘../config/webpack.config.client.dev’ we need to know the assigned port to WebpackDevServer and set the following process.env.REACT_APP_CLIENT_PORT = port

So after const urls = prepareUrls(protocol, HOST, port); will do:
process.env.REACT_APP_CLIENT_PORT = port
const configWebpackClient = require(‘../config/webpack.config.client.dev’);

2. Instead of const compiler = createCompiler(webpack, configWebpackClient, appName, urls, useYarn); we are going to use a standard compiler: const compiler = webpack(configWebpackClient);

The reason is, we don’t want custom messages since things won’t work as those messages say.

The code for the previous two points: https://github.com/leanjscom/universal-create-react-app/blob/master/scripts/start.js#L63

3. Once WebpackDevServer starts on the available port, we need to find an available port for the Express server:

choosePort(HOST, DEFAULT_SERVER_PORT).then(portServer => {
 if (portServer == null) {
 // We have not found a port.
 return;
}

4. We need to require the ‘../config/webpack.config.server’, compile and watch for changes:

// process.env.REACT_APP_SERVER_PORT is used by server/index.js
process.env.REACT_APP_SERVER_PORT = portServer;
const configWebpackServer = require(‘../config/webpack.config.server’);
const compiler = webpack(configWebpackServer);
const urls = prepareUrls(protocol, HOST, portServer);
let isServerRunning;
compiler.watch({ // watch options:
 aggregateTimeout: 300,
}, function(err, stats) {
//…

Code of the previous two points https://github.com/leanjscom/universal-create-react-app/blob/master/scripts/start.js#L93

5. We execute nodemon to watch for changes and run our server

const nodemon = exec(‘nodemon — watch build/server build/server/bundle.js build/server/bundle.js’)

Full implementation of scripts/start.js https://github.com/leanjscom/universal-create-react-app/blob/master/scripts/start.js

Step 4. Last but not least, configuration.

/config/polyfills.js

Change require(‘whatwg-fetch’) to require(‘isomorphic-fetch’)

/config/path

remove this:
- appIndexJs: resolveApp(‘src/client/index.js’),
- serverIndexJs: resolveApp(‘src/server/index.js’),
- appBuild: resolveApp(‘build/client’),
- serverBuild: resolveApp(‘build/server’),

add this:
- appIndexJs: resolveApp(‘src/index.js’)
- appBuild: resolveApp(‘build’)

Webpack configuration

We need 3 different Webpack configurations.

1- One for the client/bundle.js in production environment. We are going to use puglins like HMR that are not required on the server.

2. Another one for the client/bundle.js in development environment. We are going to use plugins like minification that are not required in development.

3- The server will have the same Webpack configuration in production and development. This is the reason we don’t require different plugins in development and production.

We are going to create a base.config, and the 3 Webpack config files will extend it. You can have a look at this file and see what the common things are: https://github.com/technology-ebay-de/universal-react-router4/blob/master/config/webpack.config.base.js

/config/webpack.config.server.js

Important things to highlight here:

We need to clone the webpack.config.base — we can use const config = Object.assign({}, base). This is because the scripts/start.js will run webpack.config.server.js and webpack.config.client.dev.js, and both override webpack.config.base

config.target = ‘node’

config.entry = ‘./src/server’ // the entry point is different from the client. Notice that Webpack doesn’t include in the bundle files that are not required or imported

config.externals = [nodeExternals()] // / in order to ignore all modules in node_modules folder

config.output = {
 path: paths.serverBuild,
 filename: ‘bundle.js’,
 publicPath: ‘/’
}

/config/webpack.config.client.dev.js

The important bits here:

config.output = {
 …
hotUpdateChunkFilename: ‘static/[id].[hash].hot-update.js’, hotUpdateMainFilename: ‘static/[hash].hot-update.json’,
}

This is because we need to proxy Webpack HMR. https://github.com/leanjscom/universal-create-react-app/blob/master/src/server/index.js#L20

config.module.rules we add the style-loader. We only include the “style-loader” on the client because it doesn’t work on the server-side

config.plugins, here we add modules like HotModuleReplacementPlugin. https://github.com/leanjscom/universal-create-react-app/blob/master/config/webpack.config.client.dev.js#L96

Some libraries import Node modules but don’t use them in the browser. Tell Webpack to provide empty mocks for them so importing them works.

config.node = {
 fs: ‘empty’,
 net: ‘empty’,
 tls: ‘empty’,
}

/config/webpack.config.client.prod.js

The important bits here:

In the config.entry we don’t need react-dev-utils/webpackHotDevClient and react-error-overlay

config.plugins: we need to add these plugins: ManifestPlugin, SWPrecacheWebpackPlugin and UglifyJsPlugin

Congrats! you read to the end of the article :)

Here you can see a full implementation of all the steps
https://github.com/leanjscom/universal-create-react-app

Happy universal hacking!

Like what you read? Give Alex Lobera a round of applause.

From a quick cheer to a standing ovation, clap to show how much you enjoyed this story.