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 thegetProps
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 asprops
. - 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 togetProps
by implementing the component’sextraGetPropsArgs
methodroutes
: 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.jsimport {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.jsxconst 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 objectreq
: the request Express object, available server-side onlyres
: the response Express object, available server-side onlylocation
: 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}