In my previous article about Server-Side Rendering in React, I have briefly described the idea of isomorphic and universal applications. I’ve also shown the advantages and disadvantages of this approach. Today, I will get to the point and show you how to configure a universal React app using the ExpressJS framework!
First, I will describe how to configure the app for the client, and then I will transform it to make it also work server-side. It will be the most basic example of the React universal application. Later, in the following issues of this series, I will expand my code by the use of Redux and react-router.
Starting point — an example React component
To achieve the above goals, we need two starting points in our app. For the server, it will be the
server.js file, and for the
client.js client, it will be the file. We will discuss these two files in a second, but first, let’s create our main React component. It may look like below (
As you can see, it is a simple component that gets some initial text from it’s
props, assigns it to the state, and then renders it — and we want it to be done on the server.
Additionally, there is a button that changes the value of the state’s
text property. This interaction will be done on the client.
Now that we have a component, we can render it. For now, let’s do it only for the client. Please see below the content of the
If you have been working with React for some time, you should be familiar with the above code. Thanks to the
render method of the
ReactDOM object we can inject the
Of course, if we want to make all of this work, we have to add a few packages to our project:
Please note that I don’t use the
--dev parameter here. It’s because I will need these packages not only in the client-side’s “bundle” but also on the server.
The next step is to prepare the
index.html file. It will have our client script attached (the browser will load it after the index file is loaded). It will also have a container with the “app” identifier where our app will be injected. To handle all of it will use webpack.
Let’s start by installing all the necessary packages:
We will also need some Babel presets (predefined sets of Babel plugins):
And last but not least, we should install Babel polyfill package (for both the client and the server so no
Promise). Thanks to
babel-polyfill we can emulate the whole ES6+ environment.
Ok, now we are ready to add the
index.html file to our project:
We will use this file as a starting point of our application — at least for now — when we add the server-side configuration, this file won’t be necessary anymore. Please note, the “app” container — our React application will be injected here.
You may also note that there is no client script attached — it is because it is not ready yet. It will be generated by webpack, and then injected to the index file using the webpack plugin called
Webpack configuration of the client part
Now, let’s configure webpack to generate the client’s bundle. To do so, we have to add the
webpack.config.js file to our project. Please see below its content:
For those who are not familiar with configuring webpack, let me shortly explain all the options configured in the above example. First, we set the
mode option that tells webpack to use its built-in optimizations.
entry section, we configure what the entry point of the bundle is. As you can see, currently, we have only one entry point called
client , and it points to the
client.js file. Please note that before loading this file, we also load the
Next, in the
output section, we can define where the output files will be placed. Here, we set the target folder to
build and the file name is
[name].js where the
[name] is a placeholder that will be replaced by the entry point name (which is just
client in our case).
The next section is called
module. Here we can configure all the loaders that will be used to transform all the specified file types. In our case, we use
babel-loader to transform
In the end, we configure plugins and optimizations. The
index.html file (we discussed it before). In the
optimization section we tell webpack to put all the vendors to (packages imported from
node_modules) the separate
Ok, now we can test our configuration. We can do it by calling the following command (being in the root folder of the project):
This will create the
build directory with all the necessary files. If you want to test it, just go into this folder and run some local web server, e.g., on Mac you can run the Python’s Simple Server:
Now open your web browser and open the http://localhost:8000 page.
In the solution I’ve just described above, we simply inject script generated by webpack into the
index.html file. When the web browser renders the HTML file, the injected script is loaded and then invoked immediately. This results in React’s component tree translated into DOM elements and placed inside the “app” container.
Now, it is time to add server-side rendering to our application. Our goal is to move the last part (rendering and placing it into the container) to the server. Thanks to that, the browser will get the
index.html file already fulfilled appropriately, and the user won’t have to wait for scripts to be loaded.
Before we start adjusting our configuration, we have to install some dependencies. First, the “dev” one:
Next, the common one for both environments:
I will explain why we need them later.
The Html React component
In the client only approach, to prepare the
index.html file, we’ve used the
HtmlWebpackPlugin. Because now we want to render the whole React component tree on the server, we have to use a more sophisticated approach. That’s why I will introduce the
Html.js component that will replace the
index.html file. Please see below how I’ve implemented it:
The above component renders more or less the same HTML structure as the
index.html. It also obtains two props:
scripts. The content of the first one is injected into the “app” container. We have to use the
dangerouslySetInnerHTML attribute because
children contains the HTML markup as a string, and we don’t want it to be escaped.
script prop is an array of URLs. We map them into a series of
script elements. This way, we will attach to our app all the client-side scripts generated by webpack.
If we have the
Html.js component created, we can safely remove the
Finally: rendering on the server
Now we can move to the most crucial part of this article. Let’s create the
server.js file. It will be an entry point of the application and will be invoked on the server based on Node.js. To make it easier to work with its network capabilities, we will use the ExpressJS framework (we added it to our project a few minutes ago).
Ok, please take a look at the
Let’s see what we have got here… First of all, please notice the import of
ReactDOMServer from the
react-dom/server package. It allows us to render the React components tree on the server. We also import a few other packages, as well as two components we’ve already created (
The next thing is to call an express method and assigning its result to the
app constant. This way, we initialize the ExpressJS framework. In the next line, we inform express, where our static files (like scripts generated by webpack) are placed.
In the following line (the
app.get call), we start the most exciting part of our example, but we will discuss in a moment. First, let’s take a look at the last line — by calling the
listen method of the
app object we start the whole application — it begins to listen on port 3000.
Ok, now let’s get back to the
app.get call mentioned above. This way, we can handle GET requests that are sent by the web browser. As a first parameter of the call, we pass the address we want to handle. In our case is an asterisk — it means that the callback function passed as a second parameter of the call will be invoked for every GET request.
The callback function takes two parameters:
req (request object) and
res (response object). We will use the second of them at the end of the method, but first, a few things happen.
First of all, we define an array containing paths to scripts generated by webpack. We will pass it to the
Html component — you may remember that there, we map this it into a series of
Secondly, we call the
renderToString method of the
ReactDOMServer object. We pass the
App component as a parameter of this method. Please note what text we assign to its
initialText attribute. This way, we render the whole React app to string and assign it to the
Thirdly, we call another method of the
ReactDOMServer object —
renderToStaticMarkup. It works almost the same as the
renderToString method. The only distinction is that
renderToStaticMarkup omits all the HTML attributes React adds to the DOM during rendering. We pass the
Html component as a parameter of the call. It takes
scripts array through its attributes. This way we wrap the React component tree we have just rendered by the HTML body and save it as a string in the
At last, we just call the
send method of the
res object. By doing this, we send the fully rendered app to the browser.
Changes to webpack config
server.js file imports React components and use the ES6+ syntax. That's why we have to run it through webpack too. Please see below, the modified
To cut the long story short, let’ focus only on the most important things here.
In the example above, we have two configurations now:
clientConfig for the client and
serverConfig for the server. Moreover, we have the
common object that contains a mutual part of the configuration. Please note how we export our config at the end of the file — using an array, we can pass more than one setting to webpack.
Regarding the client part of the config, almost nothing has changed — the only thing is that using
HtmlWebpackPlugin is not necessary anymore. Additionally, the
node property has been added — by this option, we can configure whether to polyfill or mock specific Node.js globals and modules. Please also notice how
target properties define the purpose of the output.
Now, let’s take a look at the server part of the configuration. As you can see, we share the loaders config with the client configuration (
target property points to “node” this time. We also added the
externals option defining all the dependencies that are necessary during bundling but not needed in the output file (thanks to the
webpack-node-externals library we don't have to do it on our own). Of course, the most important are two properties:
output. Besides using babel polyfills, we set the entry point to the
server.js file we had just defined a few minutes ago. The output bundle will be placed in the
Sharing initial state
Ok, now that we have the app configured for both the server and the client, we can test how it is working. Let’s remove the
bulild folder (just to make sure we start with a clean build), and call the following command:
The above command will build both the server and the client code, place it into the
build folder, and run the app. Now, open the http://localhost:3000 address in your web browser and see the result.
It is not exactly what we wanted to achieve, right? When the browser loads our application, it renders the server-side version first (you may see the “rendered on the server” text for milliseconds) and then runs the client-side code that replaces the original text by “rendered on the client.”
To fix it, we have to share the initial state of our app between server and client. To do so, let’s modify the
Html.js component code like shown below:
Two things have been added here: first of all, we have passed additional property through the props —
initialState; secondly, we have added a script that converts it into JSON format and assigns it to the
Of course, the script we have just added will be called after the page is rendered in the browser, so we don’t have to be afraid that the
window object is not defined.
Now, it’s time to modify the
server.js file too:
Let’s take a quick glance at what had changed here… Firstly, the
initialState object was introduced. Secondly, we passed it to both
You may wonder what the below statement means:
It is just usage of the spread operator to pass all the properties of the
initialState object as attributes of the
App.js component. So the above statement is equal to the one shown below:
The last thing we did is to pass the whole
initialState object to the
Html.js component. This way it will be added to the
window.APP_STATE property, as described before.
There is one last thing to do in our project. We have to get the value of the initial state in the client code. We can do this by modifying the
As we already know, this code will be invoked in the browser, so we can be sure that the
window object is available. We expect here it has the
APP_STATE property defined and using the spread operator we pass all of its properties as attributes of the
Please note the second distinction we have introduced here. We replaced the
render method of the
ReactDOM object by the call of the
hydrate method. If we pre-render React code on the server and want to connect it with the client-side React code, we have to use the
hydrate method — otherwise we will get an error in the console.
Ok, all set now! You can test it once again by calling
webpack && node ./build/server.js and opening http://localhost:3000 in the browser. This time you will see the “rendered on the server” text as expected.
I hope that despite the length of this text, I’ve reasonably explained everything. I think it is quite straightforward and I hope you won’t have problems using it for your purposes.
The example we have discussed today is available in my GitHub repository. I encourage you to clone it and play with it on your own.
In the next issue of the “Server-Side Rendering in React” series, I plan to explain how to handle Redux in our SSR example.
P.S. This post is a part of a series of articles about Server-Side Rendering using React. Please see the list of all articles of the series below:
- Server-Side Rendering in React — an introduction
- Server-Side Rendering in React — Express.js
- Server-Side Rendering in React — Redux
- Server-Side Rendering in React — react-router
- Server-Side Rendering in React — Dealing with Real Data