How to make a Universal Javascript Single-Page App with Server-Side Rendering

In this post I will explain what this is, and the basic idea of how to make one. Feel free to check out my React Redux boilerplate to see a working implementation of what I’m about to explain.

First, let’s define what a universal Javascript Single-Page app with server-side rendering is.

Single-Page App

A single-page app (SPA) is basically a website without page refreshes.

The main advantage is speed, but you can also do a hell of a lot more without page refreshes. For example, you can

  • Have media playing across pages, such as music or video. Music players have to be SPAs because otherwise the music would stop every time you navigated to a new page (my first professional job working on an SPA was on a music player website).
  • Share state in memory across pages. I did this on an ecommerce site I built to cache product data and avoid having to refetch data that’s already been obtained, saving bandwidth as well.
  • Page animations

Ultimately, SPAs are way faster and more powerful than normal websites. So why aren’t they the norm? Because they’ve historically been very complex to build. But now with libraries like React and React-Router, that is no longer the case.

Now a really simplistic way to build a SPA is to just bundle all your Javascript code on the client, send an incomplete HTML file and offload all the rendering work to the client. In fact, the first front-end frameworks like Angular 1 did this. The problem is that 1. the website loads jankier and slower with a lot of shifting elements, meaning an inferior user experience 2. terrible for SEO because search engines can’t crawl your site.

A better way is to initially render the site on the server (server-side rendering), and then for subsequent page navigations, you render on the client.

Ok, so we know that to render our views on the client, we need to use Javascript because it’s the only language web browsers accept for client-side scripting (though with Web Assembly, maybe that’ll change in the future). So how do we render our views on the server?

Before Node.js, you would’ve had to rewrite your client-side rendering logic in whatever language you were using on the backend (eg. Python). But thankfully with Node.js, you can reuse the same client-side Javascript code on the server.

Universal Javascript (a.k.a isomorphic Javascript)

This just means that your Javascript runs on both the client and the server (via Node.js). If your rendering logic is on both the client and server, then you can reuse the same code. This reduces the amount of code and leaves one source of truth, saving the developer the time and hassle of dealing with multiple codebases.

Building a SPA

Great, so now we know that we can use Node.js to build a blazing fast SPA with server-side rendering. How do we actually build one?

First of all, there are frameworks like Next.js which do a lot of the work for you (and for static websites, Gatsby). But let’s say you want to build one yourself without being tied down to a framework.

1. Transferring state from server to client

It’s important that on the initial page load, the client receives not only the fully rendered HTML, but also the application state. You can send the state from server to client by simply defining it as a global variable on the window object in your server-side HTML template, and then reading this on the client (note: you should generally never use global variables on the window object, but this is one of those cases where it’s unavoidable).

// ServerHtmlTemplate.js

<body>
  ...
  <script>
    window.__INITIAL_STATE__= ${JSON.stringify(initialState)}
  </script>
</body>
// client.js

const initialState = window.__INITIAL_STATE__
const store = configureStore(initialState) // Create Redux store using this state

In this case, window.__INITIAL_STATE__ is the global variable that stores all of your initial state data. If you’re using Redux (a popular React state management library), then you can then set your store to this initial state.

2. Asynchronous calls

Naturally you’re going to have pages where you make an API call to fetch all of the data necessary for the page. For example, a blog will have a page with a list of all of the blog posts. How do you cleanly do this in a SPA?

The way I implemented this in my boilerplate was to have an optional static fetchData() function that can be specified on any React page component and returns a Promise. Within this function, any asynchronous API calls can be specified. Then in your server and client-side rendering logic, you can first check for the existence of this fetchData() function, and if it exists, you can call it first and defer rendering until it’s complete.

However, if you are doing this, it is extremely important to set a timeout in case the API call takes forever or hangs indefinitely. Otherwise your user will be forced to sit their with a blank page until the call is finished or the connection times out. I handled this by setting a 1 second timeout (check the serverRender.js file of my boilerplate to see this implementation).

3. Code Splitting

Without code splitting, a large SPA would contain all of the code of the entire website, which could potentially be a very massive Javascript file. Thankfully with code splitting, we can avoid this and only send the code required for the current page. With webpack, this is pretty simple to implement. Bundles can be separated as follows:

  • vendor.js - This bundle will contain all of your NPM module dependencies. The idea is that it won’t change as frequently
  • main.js - This will contain all of your main application logic
  • [specific-page].js - This will contain the code for the specific page that the user is accessing.

By splitting up your code, you can increase performance and save bandwidth by only loading the code that’s necessary, while caching the code that changes infrequently.

4. The URL should be the single source of truth

One of the lessons I learned working on SPAs was that the URL should be the single source of truth. By that, I mean that any API calls or actions you take should assume that the user has come directly from the URL (excluding caching).

For example, if you have an ecommerce website with a catalog page containing a list of products and a bunch of filters, each filter should contain a valid URL, and from that URL alone you should be able to generate the page. Do not add any fancer reducer logic (Redux terminology) to make specific state changes on the PAGE_CHANGE. Just assume the user is navigating directly to the URL. Then it will always work, and as a developer the code is easier to understand and maintain anyways.

Conclusion

This is a high-level overview of Universal Javascript SPAs. Check out my React/Redux SPA boilerplate to see a working example that I created and used in production at my last job.

As it gets easier and easier to make SPAs, they will become the norm because they offer a superior user experience. Props to all the open source developers who’ve made it fairly reasonable to set up. It’ll be exciting to see where things go in the future.