An Introduction to Isomorphic Web Application Architecture
Isomorphic Web App Overview
To understand what an isomorphic web app is, we’re going to use an example web app called All Things Westies. On this site, you can find places to adopt a Westie (West Highland White Terrier, a small white breed of dog). You can also get information about Westies, purchase dog supplies and buy products featuring Westies (socks, mugs, shirts, etc.). Because this is an ecommerce app, we care about having a good Search Engine Optimization (SEO) presence and we want our customers to have a great experience using the app.
How it works
Look at Figure 1, which is a wireframe for the All Things Westies app. There’s a standard header with main site navigation on the right. Below the header, the main content areas promote products, blog posts and the social media presence.
In order to make this work, there’s a third piece in the mix. I think of this part as the “isomorphic handoff.” On the server, you save the state of the application and then provide this state to the browser. The browser uses this state to bootstrap the SPA version of the application. Without this isomorphic handoff, the user must wait for the server-rendered page to load, and then wait longer for a complete re-render of the content in the browser.
Building our stack
Make sure any libraries you include in an isomorphic app support running in both the server and browser environments.
The HTML components that display the products and supplies, i.e. the view, will be built with React. We’ll use a Flux-like data architecture via Redux, the current community standard for Flux-like data management in React apps. We’ll explore using webpack to manage what code runs in the browser and to enable running Node module code in the browser.
On the server side, we’ll build a Node.js server using Express to handle routing. We’ll take advantage of React’s ability to render on the server, and use it to build up a complete HTML response that can be served to the browser. Figure 3 shows how all these pieces fit together.
To make our application work everywhere, we’ll build them in a way that pre-fetches data for our routes using React Router. We’ll also handle differences in environments by building separate code entry points for the server and browser. In cases where code can only be run in the browser, we’ll gate the code or take advantage of the React lifecycle to ensure code won’t run on the server.
Earlier in this article, I told you how an isomorphic application is the combination of a server-rendered application and a single-page application, and uses both in the same application architecture. To get a better understanding of how we connect the concepts of a server-rendered application and a single-page application, let’s refer to Figure 4.
Figure 4 shows all the steps involved in getting an isomorphic app rendered and responding to user input, like a single-page application, starting when the user enters the web address.
Every web app session is initiated when a user navigates to the web app or types the URL into the browser window. For allthingswesties.com, when a user clicks on a link to the app from an email or from searching on Google, the flow on the server goes through the following steps.
- The server receives a request.
- Our application router handles the request and gathers the data required for the part of our application being requested. If the request is for allthingswesties.com/mugs, the app requests the list of mugs for sale through the site. This list of mugs, along with information to be displayed
(names, descriptions, price, images), is collected before moving on to the render step.
- The server generates the HTML for our web page using the data collected for the mugs page.
- The server responds to the request for allthingswesties.com/mugs with the fully-built HTML.
The next part of the application cycles the initial load in the browser. We differentiate the first time the user loads the app from subsequent requests because several things will only happen once during this first load.
Initial load is the first time the user interacts with our website. This means the first time the user clicks a link to our site in a Google search, from social media or types it directly into the web address bar.
The first load on the browser begins as soon as the HTML response from the server’s received and the DOM is processed.
- The browser starts to render the mugs page immediately because the HTML sent by the server is fully formed, with all of the content we generated on the server. This includes the header and the footer of our app, along with the list of mugs for purchase. The app won’t respond to user input at this point, like adding a mug to the cart, or viewing the detail page for a specific mug.
- The virtual DOM is recreated in React. Because the server sent down the app state, this virtual DOM is identical to the current DOM.
- Nothing happens! React finds no differences between the DOM and the virtual DOM it built. The user is already being shown the list of mugs. The application can respond to user input now, like adding a mug to the cart.
At this point, single-page application flow takes over and the app responds to user input, browser events, timers, etc. The user can add products to their cart, navigate around the site, and interact with complex slideshows for pictures of products and dogs.
- The application responds to user input, like clicking add to cart.
- The virtual DOM is rebuilt once any calls have been made to our cart APIs.
- React diffs the virtual and browser DOM
- Updates are made and any repaints are executed. The users cart icon updates to show that an item has been added.
Handling the server-side request
Now let’s dive in a little deeper and take a closer look at what happens when the server receives the initial request to render the page. First let’s look at what part of the site renders on the server. Figure 5 is like the one at the beginning of the article (Figure 1) but it doesn’t render the twitter widget. The Twitter widget is designed to be loaded in the browser and doesn’t render on the server.
The server does two important things. First, it fetches the data required for the view. Then it takes that data and uses it to render the DOM. Let’s check out Figure 6, which shows the flow on the server.
- The server receives a request.
- The server fetches the required data for that request. This can be from either a persistent data store like a MySQL or NoSQL database, or from an external API.
- Once the data are received, the server can build the HTML. It generates the markup with React’s virtual DOM via the renderToString method.
- The server injects the data from step 2 into our HTML, allowing the browser to access it later.
- The server responds to the request with our fully built HTML.
Rendering in the Browser
Now let’s look more closely at what happens on the browser. Figure 7 shows the flow in the browser, from the point the browser receives the html to the point it bootstraps the app:
- The browser parses the DOM that it has received from the server;
- This results in rendering an HTML element; or
- When the browser reaches our entry point for the application, the app bootstraps itself.
At this point our single-page application flow kicks in again. This is the most straightforward part. It handles user events, makes XHR calls and updates the application as needed.