Demystifying Server-Side Rendering in React
A closer look at the feature that allows you to build universal applications with React
Server-Side Rendering — SSR from here on — is the ability of a front-end framework to render markup while running on a back-end system.
Applications that have the ability to render both on the server and on the client are called universal apps.
In order to understand why SSR is needed, we need to understand the evolution of web applications in the past ten years.
This is tightly coupled with the rise of the Single Page Application — SPA from here on. SPAs offer great advantages in speed and UX over traditional server-rendered apps.
This means that the user will have to wait longer for the initial render. This also means that crawlers may interpret your page as empty.
So the idea is to render your app on the server initially, then to leverage the capabilities of SPAs on the client.
SSR + SPA = Universal App*
*You will find the term isomorphic app in some articles — it’s the same thing.
Now the user does not have to wait for your JS to load and gets a fully rendered HTML as soon as the initial request returns a response.
Imagine the huge improvement for users navigating on slow 3G networks. Rather than waiting for over 20 seconds for the website to load, they get content on their screen almost instantly.
And now, all the requests that are made to your server return fully rendered HTML. Great news for your SEO department!
Crawlers will now see your website as any other static site on the web and will index all the content you render on the server.
So to recap, the two main benefits we get from SSR are:
- Faster times for the initial page render
- Fully indexable HTML pages
Understanding SSR — One Step at a Time
Let’s take an iterative approach to build our complete SSR example. We start with React’s API for server rendering and we’ll add something to the mix at each step.
You can follow this repository and the tags defined there for each step.
First things first. In order to use SSR, we need a server! We’ll use a simple Express app that will render our React app.
We need to tell Express to serve our static files from our output folder — line 10.
We create a route that handles all non-static incoming requests. This route will respond with the rendered HTML.
renderToString — lines 13–14 — to convert our starting JSX into a
string that we insert in the HTML template.
As a note, we’re using the same Babel plugins for the client code and for the server code. So JSX and ES Modules work inside
The corresponding method on the client is now
ReactDOM.hydrate. This function will use the server-rendered React app and will attach event handlers.
To see the full example, check out the
basic tag in the repository.
That’s it! You just created your first server-rendered React app!
We have to be honest here, the app doesn’t do much. So let’s add a few routes and see how we handle the server part.
Layout component now renders multiple routes on the client.
We need to mimic the Router setup on the server. Below you can see the main changes that should be done.
On the server, we need to wrap our React application in the
StaticRouter component and provide the
As a side note, the
context is used for tracking potential redirects while rendering the React DOM. This needs to be handled with a 3XX response from the server.
The full example can be seen on the
router tag in the same repository.
Now that we have routing capabilities, let’s integrate Redux.
In the simple scenario, we need Redux to handle state management on the client. But what if we need to render parts of the DOM based on that state? It makes sense to initialize Redux on the server.
If your app is dispatching actions on the server, it needs to capture the state and send it over the wire together with the HTML. On the client, we feed that initial state into Redux.
Let’s have a look at the server first:
It looks ugly, but we need to send the full JSON state together with our HTML.
Then we look at the client:
Notice that we call
createStore twice, first on the server, then on the client. However, on the client, we initialize the state with whatever state was saved on the server. This process is similar to the DOM hydration.
The full example can be seen on the
redux tag in the same repository.
The final piece of the puzzle is loading data. This is where it gets a bit trickier. Let’s say we have an API serving JSON data.
In our codebase, I fetch all the events from the 2018 Formula 1 season from a public API. Let’s say we want to display all the events on the Home page.
We can call our API only from the client after the React app is mounted and everything is rendered. But this will have a bad impact on UX, potentially showing a spinner or a loader before the user sees relevant content.
We already have Redux as a way of storing data on the server and sending it over to the client.
What if we make our API calls on the server, store the results in Redux, and then render the full HTML with the relevant data for the client?
But how can we know which calls need to be made?
First, we need a different way of declaring routes. So we switch to the so-called routes config file.
And we statically declare the data requirements on each component.
Keep in mind that
serverFetch is made up—you can use whatever sounds better for you.
As a note here,
fetchData is a Redux thunk action, returning a Promise when dispatched.
On the server, we can use a special function from
With this, we get a list of components that will be mounted when React is rendered to string on the current URL.
We gather the data requirements and we wait for all the API calls to return. Finally, we resume the server render, but with data already available in Redux.
The full example can be seen on the
fetch-data tag in the same repository.
You’ll probably notice that this comes with a performance penalty, because we’re delaying the render until the data is fetched.
This is where you start comparing metrics and do your best to understand which calls are essential and which aren’t. For example, fetching products for an ecommerce app might be crucial, but prices and sidebar filters can be lazy loaded.
As a bonus, let’s look at SEO. While working with React, you may want to set different values in your
<head> tag. For example, you may want to set the title, meta tags, keywords, and so on.
Keep in mind that the
<head> tag is normally not part of your React app!
react-helmet has you covered in this scenario. And it has great support for SSR.
You just add your
head data anywhere in your component tree. This gives you support for changing values outside the mounted React app on the client.
And now we add the support for SSR:
And now we have a fully functional React SSR example!
We started from a simple render of HTML in the context of an Express app. We gradually added routing, state management, and data fetching. Finally, we handled changes outside the scope of the React application.
The final codebase is on
master on the same repository that was mentioned before.
As you’ve seen, SSR is not a big deal, but it can get complex. And it’s much easier to grasp if you build your needs step by step.
Is it worth adding SSR to your application? As always, it depends. It’s a must if your website is public and accessible to hundreds of thousands of users. But if you’re building a tool/dashboard-like application it might not be worth the effort.
However, leveraging the power of universal apps is a step forward for the front-end community.
Do you use a similar approach for SSR? Or you think I missed something? Drop me a message below.
If you found this article useful, help me share it with the community!