At my work I use React as the primary UI framework, adoption started with the service site, a huge web application (hundreds of screens and dozens of flows and wizards) that allows users and admins to configure the system. Successful technologies and approaches tend to eventually propagate more and more to other products of the company, so the React expansion was not a surprise and now React stack is used as a standard approach.
Unfortunately, there is no silver bullet, requirements differ between projects, so at some point it became obvious that we no longer can live with just simple client side rendering and should take a look on a server side rendering.
This is one of the articles about the research that has been done in scope of React, Redux, React Router and other React-* technology stack in conjunction with Server Side Rendering approach.
Server Side Rendering sequence in a nutshell
There is a whole new class of challenges in client-side single page apps, but the main issue always was the start up time. Users have to wait while the initial HTML loads, then CSS and JS, then app bootstraps, then it renders, then data is fetched, then it renders again with data. Only at this point the app becomes usable. Server side rendering allows to deliver more stuff up-front. This becomes crucial if we make flows that are embedded into native mobile apps, where we want seamless experience between native and web.
Number one objective is to reach the certain page, obey all the rules defined in React Router’s routes, then dispatch some actions, then render the page and send it to client. Fairly straightforward.
Client requires some preparations in order to be correctly server-rendered.
I highly recommend to add so-called code split points so that each page will be a separate Webpack chunk. This will allow to download only necessary stuff when client is initializing.
Get rid of anything related to window object or global scope (all your location, history, document, direct DOM manipulation, etc.). There is no such stuff on a server and usage of global is a very bad practice by definition. You will surely get an overlap between requests your code has shared state somewhere except the Redux Store. Global can be used very carefully and only if you really understand why you need it.
Make sure your client code does not leak. Every memory leak will become a problem when it’s no longer a client browser with tons of memory, but even there it’s a big no-no. On a server with very limited resources and potentially high load it will become crucial.
Check your 3rd party libraries, they can be unaware of the terms “universal” or “isomorphic”.
This is probably the simplest part, we only need to create a function that will return routes.
Some folks export an instance of Redux Store, so it acts like an app-wide singleton. That’s not the case for server rendering where each request is unique and should have personal Redux Store. So we should follow best practices and export afunction instead:
Initial state must be an argument because we will use it later.
Router allows the server or client to reach the main content page. Usually it has the list of things or a certain content like a blog post or an article. We’d like to dispatch some actions before we ship the resulting HTML to the client, for example, load that list or article. We can do it in a special static method of the endpoint Component. We will specifically follow the Next.js-alike naming:. In this method we can prepare our Redux Store’s so that when component will be rendered, the will have all the right things, so the resulting HTML will have them too.
The page should look like this:
The leaf node for 404 pages should also have some static property to determine that it’s an error:should work:
Now we need to render the app. The hackery is especially required if you use async routes, those have to be parsed first and only after that they can be rendered, otherwise you’ll get a blank page.
Here is the example of the main app endpoint:
Here is the samplethat should be a part of webpack build, e.g. emitted to you output path. It could be a real file or generated by .
Server has to do pretty much the same thing as client side, but with one major difference: it should have a unique Redux Store for each request, unlike the client, where the Store is a singleton.
When request comes to the server:
it performs the same matching logic of Routes to determine the endpoint page, if nothing matches, it tries to serve as static
creates the fresh Redux
waits until all async actions will be dispatched
injects it into template HTML
renders React app to an HTML string
sends everything to client
Step 5 is absolutely crucial because otherwise the client side will not be applied correctly on initial HTML and you will get a nasty warning that you’re loosing all benefits of server rendering since the app has just been rendered from scratch in order to avoid inconsistency.
We assume that you should already have, and . You will need them.
Server rendering basically takes your application, applies same Babel transformations just like Webpack does, then renders it to an HTML string. Because of that you will also need asince it's the easiest way to run the server with Babel:
We assume that you either havefile or section in your so that Webpack’s Babel and NodeJS Babel have same config. Keep in mind that if you use , then you can only use it in Webpack config, Babel config of Node should not have , instead you should use and (first replaces , second changes to regular synchronous ).
Alter yourand add to section:
For convenienceshould have section, where port and content base should be specified, also we should add HtmlWebpackPlugin to section:
React Router Redux Server Rendering Middleware
We would like to use benefits thatprovides, so our server side will have two modes: development and production. At the same time we will use same rendering mechanism for both modes: real file system for production and memory file system for development. This is also a responsibility of middleware.
Now let’s set up the, you can add more customizations if needed.
Server Side Renderer middleware
Once we have the basic static server we can import and configure the middleware:
Please note thatfunction also can do more sophisticated things like finding tags (or taking it from or ) and replacing tag with it’s content, this is good for SEO. Also instead of simple you may use any template engine you like. The resulting return should be a plain string.
You can also providefor server-side errors when client app was not even rendered.
Next we rewrite the sections where we created servers:
Room for improvements
Recursively check the component tree looking for
The goal is to have completely rendered website, not just content area. There is an existing issue with synchronization of all such calls, the order has to be always predictable.
Pre-built server-side scripts
First of all, we can have a second Webpack config (or a multi-config, when we return an array of configs instead of one config object), that will haveand build it along with client side. In this case we won’t have to bother about non-js extensions, Babel in runtime, and so on, everything will be pre-built and then executed in a plain mode.
The entry point has to only exportand functions, the rest will come along.