Server Side Rendering (SSR) of Create-React-App (CRA) app in 2020
February 19, 2020
If you do a Google search on how to do SSR of a CRA app you will find a bunch of links. I would know, because I did it myself :)
I had a React app (created with CRA) using Redux to store its state and react-router to handle routes. And for that project, SEO and a good Lighthouse score were essentials. With that in mind, I searched for a way to add SSR to that app without major changes or refactors.
What I found can be classified in three categories:
1. Recent articles that were incomplete (from my point of view) because they don’t include Redux or routing in the solution.
2. Old articles (~2017) that, although are complete, aren’t usable in current versions of CRA and their dependencies.
3. A bunch of github modules/projects that are very old/unmaintained, giving birth to other approaches like Razzle or just suggesting you use more mature tools to handle SSR of React apps, like Next.js and GatsbyJS.
Long story short, I couldn’t find a simple way to do SSR of a CRA app that handles the state via Redux and also has routes. Because of this, I decided to add another article to the bunch of existing ones, albeit an aggiorned one!.
I recommend that you download the sample application from here before reading the article, so you can review it as you go along.
Step one - SSR 101
My idea is try to start simple: Express (just out of familiarity) will receive the first request to our app and render the template of the CRA app (
/build/index.html) with the React App component (
/src/App.js) rendered server side.
After that, the React app will continue the execution on the client side. So, the first thing I need to add to my CRA app is Express in order to have a web server to handle that first request. Then, I have to create a server folder to include all server side processing files.
Inside it you will have an
index.js file to configure Express routes and a
bootstrap.js file where the execution will start through
node server/bootstrap.js. This bootstrap file will do most of the “magic” for the SSR.
First I add ignore-styles, that is a “babel/register style hook to ignore style imports when running in Node”, to avoid nasty errors like
^ SyntaxError: Unexpected token <.
The next line is the most controversial part of our solution: the use of @babel/register. This module “automatically compile files on the fly”. So after requiring it, all files required by Node with the extensions .es6, .es, .jsx, .mjs, and .js will be transformed by Babel. It is controversial because its use in production is discouraged (see here, here, here and babel page, selecting Require hook as examples) mostly because “it will always make your app slower and memory-consuming”.
Although that is the official position according to this, it also says: “That may be a fine tradeoff for you. The issue is startup performance”, matching what is stated here: “Is that only a one-time cost only which occurs at the application startup all at once?”.
So this is my position on this issue. I’m doing SSR to be good at SEO and getting a good score at Lighthouse. And you know that the latter, in any reasonable production scenario with several requests per second, needs a good caching solution. So if you have a good caching solution, it's pretty hard for pages with a high visitor rate to miss the cache and hit the server directly; and in those cases... It's a small price I’m willing to pay.
After this resolution (and accepting to use @babel/register), I will describe (shortly) its configuration. Presets are sets of plugins used to support particular language features without needing to micromanage which syntax transforms (and optionally, browser polyfills) are needed by your target environment(s). I use
preset-react to do the same but with React. I also use the plugin
transform-assets to “Transform importing of asset files at compile time using Babel (...) building universal apps”. As a rule of thumb, any static file extension that you would use inside your React app should be here. Finally, our
/server/index.js, which will add a get route to catch almost every server request and pass them through the
reactRenderer will do the part of the server side rendering. It will read the file
/build/index.html, render the App component to a string using
react-dom module and then replace the
<div id="root"></div> part of the template with the stringified App component. Finally, all that big HTML will be written to the response and voila!, you have SSR of a CRA app with static assets. Note the not-so-smaller detail that you have two places where the App component is rendered: one in the reactRenderer (as seen before) and the other in the
/src/index.js, that is usual for a CRA app. Take this into account for the next session.
Step two - SSR taking care of the app state
Usually your React app will handle a state in a central store, so each component can access any state that it needs from this store. In our case I will be using Redux for this.
Why does this matter in the context of SSR? Because the server will have to initialize that state, use it to render the app server-side and then send it to the client, so the app can continue the execution client-side. To test this, I implemented the Todo example app of the Redux website.
The first thing I have to change from that example code is how it uses the Provider component from react-redux. The example (and the documentation) suggest this:
But remember that I said we have two places where we render the App component, so to avoid the duplication of the Provider code, I will add the Provider invocation inside the App component. After this small change, let’s see what else we need. Action creators, reducers, presentational components (a small rename of
components/Home to avoid confusions) and container components just remain as in the example.
What happens if I try to run this app from server side, setting an initial state like this?:
If I do it, it will run smoothly server side, rendering the App with this state and adding one first todo item to the list. But as soon as the React app wants to continue working on the client side, it will do so with an empty state. To be sure that the client will continue with the same state as the server, you need to “write it” to the
react-renderer output (Remember that big HTML we talked about in step one?).
The initial state needs to be only in the
react-renderer.js file because the flow will always start server-side. It will be used to initialize the server-side store and start the rendering. After the rendering ends the store may have changed, so we need to get its state and send it to client-side.
And you may be asking yourself “What is that
__REDUX__ token?” That is a kind of anchor that I leave in the CRA
/public/index.html file to be replaced with the server side state.
I need to make one more change to the
/src/App.js file. Because we will be loading the App component from the server and client side, and because the store is being initialized differently on each side, I need to send it to the App component as a prop.
The final part of the trick is back on the
/src/index.js file. Due to this file being executed on the client-side, I need to be sure that the store uses the
window.__INITIAL_STATE__ as its initialState. That, and the use of the hydrate function (instead of the classic render) will ensure that the flow continues client-side without page re-rendering.
Step three - CRA app routes handled server-side
The last part of the challenge is to handle the internal React routes server-side. Obviously it can be done at the Express level, listening for each route and rendering the right component… but this logic would be duplicated inside the React app to make it work client-side. So, we need to handle everything inside the React app without adding anything else at the Express level. I chose to use
react-router but the approach will be the same no matter what you use for internal routing.
As stated here, to make
react-router work you should wrap your routes declaration (using
<Route /> component) in the
<Router /> component. Client-side, it makes sense to use BrowserRouter component “that uses the HTML5 history API (pushState, replaceState and the popstate event) to keep your UI in sync with the URL”. But what happens on an SSR scenario, where you don’t have a history API?
In an SSR scenario you can use the
<StaticRouter /> component. This router is specially designed for “server-side rendering scenarios when the user isn’t actually clicking around, so the location never actually changes”. Also it is helpful to write tests, hardcoding routes as a component prop. Here is the full code of the App component once refactored to handle routes.
As you can see, the App component knows if it’s rendering server or client side because of the location prop. If you load the App component with location (as I do in
react-renderer.js), then it will use the StaticRouter. Otherwise, it will assume that it is loading client-side and will use the BrowserRouter.
The last part of this is related with ‘404 Page not found’ handling. As you can see in the last image, I have a component called
NoMatch that simply shows a friendly 404 message. Remember that in step 1 we added a
/* route at the very beginning of the
/server/index.js file, although the Express documentation says that static middleware should go first.
The order of route evaluation in a common Express app is: first check that the request is a static file; if not, then look for some route that can handle the requested URL and no one can handle it, show a 404. In my case, I need to change this. Every request should be first evaluated by the Express router and sent through the React router. If React doesn't know about that route, the
reactRenderer middleware flags the request as a potential 404. Then it follows the execution (calling middleware next param) and asks Express if the request is for a static file or not. Finally, if no one knows the requested URL, it calls the
reactRenderer again, but this time to show the React app’s 404 component.
As we can see, getting a CRA app to completely work on an SSR scenario was not simple (at the beginning of this small research) but now, if you read the article and see the example GitHub project, it is pretty straightforward. However, while it works, it is fragile and error-prone and will always have to take the SSR scenario into account in every decision during the development project. We will have to wait until React offers an official SSR solution via CRA or maybe the CRA team can add it (although I wouldn’t bet on it).