React Server Side Rendering doesn’t really need to be that complicated…
I set out to prove that I could go straight from a Create React App site to SSR without adding a bunch of 3rd party libs. Makes it much easier to understand exactly what is going on.
Most of the other React SSR articles I see use Redux, but not every app uses Redux. I didn’t want to impose that design restriction on making SSR work.
I want to show that React SSR can work:
without Next.js
without Redux
The Plan
Start with a React project started from Create React App that has some API calls, defines title tag, and a couple routes
Show what problem we are trying to solve and how we would like to solve it
Evolve the app into a React Server Side Rendering, showing the challenges encountered along the way
The App
Title Tags
On the main page, I want it to have the title tag “Star Wars Heroes - List”
On the Detail page I want to have it include the name of the hero, like “Star Wars Heroes - Luke Skywalker”
API Calls
On the main list page, we call /heroes to get a list of all the heroes.
On the individual detail page, we call /heroes/0 to get a single hero.
As Yoda famously says…
Code
The code is on Github at Zerrtech/zerrtech-react-ssr-demo
4 branches, 1 for each iteration
Step 1: Initial App Demo
Branch: step-1-normal-app
To most easily see what the page fetched from the server contains, pop open your dev tools (I’m using Chrome) and click on the very first request. Check out the Preview tab which will render it as HTML. Ummm, yeah, it’s blank. It’s supposed to be.
This empty initial page load is by design.
It’s a Single Page App (SPA) after all.
React renders the app into the div id=”root”
What’s wrong with that?
Google can crawl Javascript on a page, but it takes extra steps and can delay full indexing since they have to wait for rendering by doing two passes.
Performance wise, one trip to the server is quicker than N trips to the server to generate a page, especially in mobile where the maximum number of parallel requests is smaller and connections are slower.
Combined with SSR caching, this can mean that each page is only rendered once and used by many users. Loading indicators added while waiting for API calls wouldn’t really show up.
One negative with SSR is that it will take more time to get to the first usable content. After all, delivering a blank HTML page is pretty quick. Then you can use loading indicators to indicate progress. This is opposed to waiting until the entire page is rendered before showing anything.
Rendering a HTML-only page
Image from SEOpressor
Rendering a Javascript Single Page App
Image from SEOpressor
Google uses Chrome 41 to crawl your pages.
I have version 71
Take a look at the differences by looking at Can I Use - 41 vs. 71
Use the Google Search Console to Fetch as Google (or new Search Console URL Inspection). You need to be the owner of your domain to do this, can’t just check out someone else’s page.
Read up on this at the Google Search Rendering Guide.
Why don’t all sites do Server Side Rendering then?
It’s good for content-heavy, largely publicly accessible apps, where SEO is hugely important.
If most of your content is behind a login, then SSR can’t get at it either.
A proper SSR implementation would use a layer of caching, so if you have data that absolutely needs to be as up to date as possible, there will be some tuning to do with caching vs. up to date data access.
Requires redesigning data fetching within components, ideal solution is specific to your app, no generic solution
Wait!!! Weren’t all sites like this in 1999?
How does React rendering normally work?
A user types your home page URL into the browser
Browser sends a request to the server for your home page
HTML for home page is returned to Browser, but has a blank content body and links to scripts/CSS
React boots up in Browser, renders the home page
Browser React component may fire off API requests that when they return, causes another React render
How does React Server Side Rendering work?
A user types your home page URL into the browser
Browser sends a request to the server for your home page
Server fetches data needed for the home page, seeds your component state, renders your React components, gets the HTML created by them, stuffs it into your React root in index.html and returns it to the browser
React boots up in Browser, realizes everything was already rendered so has no changes (virtual DOM)
Browser React component does not need to fire off API calls because they were already done on the server
Step 2: Initial SSR
Branch: step-2-ssr-initial
Notice here that the difference is that the HTML page in the debugger shows “Loading…”, our loading indicator. Yeah, I know, not much change. Just stick with me, we’re building this thing step by step.
We aren’t doing any API calls on the server in this initial step. But we got some React rendered by the server, enough to render our loading indicator.
Initial SSR Code Changes
In this step I added the server script. It is:
NodeJS Express
Babel compiled
Uses React DOM Server renderToString and StaticRouter
You first build the React production build the usual way: yarn build
Then the SSR server is started using: yarn start:ssr
Static files are served out of the build directory
We load our Routes under StaticRouter instead of BrowserRouter that we would normally use in the React frontend.
We grab our index.html and stuff our HTML in
Initial SSR Review
Notice no API calls were done within SSR
The render is synchronous, makes one pass through then returns what it has
Fact: when doing SSR, componentDidMount is not called
We need to do our API calls on the server, then seed our state within our component so SSR can do the full page
Step 3: SSR Server Seed
Branch: step-3-ssr-server-seed
Now we can see in the preview window that all of our content is there on the initial page load, and the title tag is there too! Booyah!
SSR Server Seed Code Changes
Added fetching data into the server script by using a static method on each route component
Pass the data into the component using StaticRouter context param
Render Helmet and replace in HTML
SSR Server Seed Data Fetching
Static method called getInitialState() added into Route component. Static so it can be called on the server easily.
Server looks for a matching route and a getInitialState method, if so calls it
We wait for API calls to be done then add data to context passed into StaticRouter
Back in the Route component, we look for that data and merge into state in the constructor.
staticContext is provided by React router withRouter()
SSR Server Seed Review
OK great, but now we are wasting our API call on the frontend, how do we prevent that?
We’ll put that same initialState in our index.html so on the client side, we can also seed that data and avoid the API call
Step 4: SSR API
Branch: step-4-api
SSR API Code Changes
Add a placeholder in index.html we can stuff our initial state into
Check for this existing data in our static getInitialState() method
Delete the data after we use it so other pages don’t use it
SSR API - API Call Saving
Add a variable in index.html
Server will stuff initialState into that var
HeroList getInitialState(matchParams) checks for existing data
React Server Side Rendering Summary
Demo of React SSR from create react app without ejecting webpack config
Showed how to use SSR on an app that has async API calls and dynamic title tags
Implemented where SSR is used and we also eliminate the client side API calls for max efficiency
AS GOOD AS 1999? DEFINITELY!
Next Steps
Great opportunity to build a generic SSR React component for Route components to inherit from
Encapsulates all that window and staticContext stuff to make it easy on devs.
Put caching in front of SSR so that you aren’t running SSR on the page every time
React Suspense API has high hopes to make it easier to identify those async API calls and make SSR easier