menu
Menu
Mobify DevCenter
search_icon_focus

Universal React Rendering

Introduction

To build the fastest-possible Progressive Web Apps (PWAs), the Mobify Platform uses a universal rendering framework built on top of React.

In universal or isomorphic rendering, applications run on both the client-side and the server-side. With this approach, you get the best of both rendering techniques: server-side rendering allows for near-instant initial page loads, and client-side rendering provides super-fast page transitions. To maximize PWA performance, Mobify applications use server-side rendering for the first page load and then client-side rendering for subsequent page loads.

Mobify’s Universal React Rendering framework allows you to write and maintain one code base that can render on both the client and the server.

In this article, we’ll outline the key concepts for using this framework, including the getProps static method and special components.

Mobify’s React Server-Side Rendering Framework

Server-side rendering is the first process that happens when a Mobify Platform application is run for the first time. Your React application runs on Mobify’s App Server as an Express app.

As a developer using this framework, it’s important to know that:

  • Server-side rendering runs in the context of an Express app
  • You will call the React rendering function, render from Mobify’s Progressive Web SDK
  • When render handles a request, it gets your route components and finds the first one that matches the incoming request path
  • If the request matches a route, the render calls the getProps static method on the route component. It waits for the returned Promise to settle, and then renders the component by passing the resolved value to it as props.
  • The SDK contains Special Components that help you extend the default server-side rendering behavior

Special Components

Mobify’s Progressive Web SDK includes Special Components that allow you to extend and override many of default behaviors of your application. Special components include:

  • _pwa-document: allows you to customize the HTML surrounding your application such as the <html>, <head>, and <body> tags
  • _error: the component that gets rendered when errors are thrown within your application on server-side and client-side
  • _app-config: customizes the app. You can customize the arguments sent to getProps by implementing the component’s extraGetPropsArgs method
  • routes: responsible for matching request URLs to corresponding React components
  • _pwa-app: provides generic functionality for the entire app

To customize your application, you can override the default versions of these files in the Project Scaffold. You’ll find the _error, _app-config, and _pwa-app components in app/components/. The routes component is in app/routes.jsx.

Running server-side in your Express application

The render function from the Progressive Web SDK handles React rendering on the server side. To use it, assign it to a Express route in your application, like this:

// app/ssr.js
import {createApp, createHandler} from 'progressive-web-sdk/dist/ssr/server/express'
import {render} from 'progressive-web-sdk/dist/ssr/server/react-rendering'
const app = createApp()
app.get('/*', render)
export const get = createHandler(app)

This will now handle any request that comes in.

So what exactly does the render function do? First, it looks at your route components.

What are route components?

In the Mobify Platform, route components are a type of Special Component included in the SDK, named routes. They’re an array of objects used to match request URLs to corresponding React components. Each object defines a path (string), a component, and the optional exact flag (boolean).

For the matching to occur, your route components must be defined in the file app/routes.jsx.

Route components are implemented using React Router 5. They look like this code snippet from your routes.jsx file:

// app/routes.jsx
const routes = [
{
path: '/products/:productId',
component: ProductDetails
},
{
path: '/',
component: Home,
exact: true
}
]
export default routes

Route components are matched from top to bottom within the array by comparing the request’s path to the route’s path. If a route doesn’t match, the router doesn’t proceed.

If a route component is matched, the getProps static method of its component is called. Take a look at our example below, which uses the Isomorphic Fetch library:

import React from 'react'
import fetch from 'isomorphic-fetch'
const ProductDetails = ({name}) => <h1>{name}</h1>
ProductList.getProps = async ({params}) => {
const response = await fetch(`https://api.example.com/products/${params.productId}`)
const data = await response.json()
return {name: data.name}
}

getProps is passed a single argument with properties:

  • params: the parameters Express object object
  • req: the request Express object, available server-side only
  • res: the response Express object, available server-side only
  • location: the URL of the request

A component’s implementation of getProps must run both server and client side, and must return a promise which resolves to an object. The object is passed as props to your component’s render method.

On the server side, this returned object is actually going to be serialized in the output html (look for __PRELOADED_STATE__). So to keep the html lean, it’s best for you to be selective in what data to return in getProps. Avoid returning the whole response from the backend if you don’t need to.

On the client side, when you navigate to subsequent pages, please note that the page would get rendered immediately, while getProps is still fetching the data. So you’ll need to check for undefined props in your page component. Remember to also render placeholder skeletons when some data is not available yet.

Extending getProps with _app-config

You can extend the set of arguments passed to getProps by extending the extraGetPropsArgs function within the _app-config component, located at app/components/_app-config/index.jsx. Remember that _app-config is a Special Component. If your application doesn’t have the _app-config file, it will default to the code in the SDK without any extended behaviour. The Project Scaffold includes a starting point to extend _app-config.

Using _pwa-app to provide global functionality

There may be some functionality that you want across all of your pages. For example, you may want a category menu that behaves the same across all pages in your app.

The _pwa-app Special Component has a getProps component of its own that is called every time a route component is matched. Use it to handle any logic that you’d like to run globally on every single page.

Error handling

No route matched

If no match is found within routes, the _error special component is rendered, and a HTTP 404 is returned. The _error component is found in app/components/_error/index.jsx.

The getProps method fails

A route component may be matched successfully, but unexpected errors can occur in getProps. It may fail because of a syntax error or because of a promise that rejects due to a timeout or other error. Imagine if we make a request to another server, and that server returns errors:

import React from 'react'
import {HTTPError} from 'progressive-web-sdk/dist/ssr/universal/errors'
import fetch from 'isomorphic-fetch'
const ProductDetails = ({name}) => <h1>{name}</h1>
ProductDetails.getProps = async () => {
const response = await fetch(`https://httpbin.org/status/500`)
if (!response.ok) {
throw new HTTPError(response.status, response.statusText)
}
const data = await response.json()
return {name: data.name}
}

There are two ways you can handle these errors:

The first option is to handle the error is to render the _error Special Component. Throw a HTTPError from progressive-web-sdk/dist/ssr/universal/errors to do this.

The second option is to create a custom error. Instead of throwing, resolve with props to inform render that there was an error:

import React from 'react'
import fetch from 'isomorphic-fetch'
const ProductDetails = ({name, errorProductNotFound}) => (
<h1>{errorProductNotFound ? 'Product Not Found' : name}</h1>
)
ProductDetails.getProps = async ({res}) => {
const response = await fetch(`https://httpbin.org/status/404`)
if (!response.ok) {
if (response.status !== 404) {
throw new HTTPError(response.status, response.statusText)
}
res && res.status(404)
return {errorProductNotFound: true}
}
const data = await response.json()
return data
}