Routing

This guide explains how to define client and server side routes using React Storefront's router.

React Storefront's Isomorphic Router

Since React Storefront supports Server-Side Rendering (SSR) by default, all pages in the PWA can either be rendered on the server (as happens when a user initially lands on the site), or on the client (all subsequent navigation). In order to keep your code DRY, React Storefront's router provides an isomorphic API that allows you to define routes that can be run on the client or server in a single place.

It is still critical, however, to keep some parts of your application code on server:

  • To protect secrets such as API keys and intellectual property
  • To keep the client bundle size as small as possible to ensure the best performance.

React Storefront's router allows you to specifically define which parts of your code run on the server and which run on the client.

Here's an example of the syntax:

import { Router, fromClient, fromServer, cache } from 'react-storefront/router'

new Router()
  .get('/product/:id', 
    cache({ 
      server: { maxAgeSeconds: 300 } // cache the result for 5 minutes on the server 
      client: true // cache on the client using the service worker as well
    }),

    // Display the product view.  If there is a custom skeleton configured for products, it will be displayed immediately while product data is fetched from the server.
    fromClient({ page: 'Product' }),

    // Fetch the product data from the server
    fromServer('./product/product-handler')
  )

Routes are typically defined in src/routes.js.

Anatomy of a Route

Every route has three parts: a method, path, and handlers.

Method

When creating a route, the HTTP method is set by using the corresponding function on Router. The supported methods are:

  • get
  • post
  • put
  • patch
  • delete
  • options

Path

React Storefront's Router has a flexible syntax for matching URL paths and extracting variables. The path can contain any of the following:

ExampleDescription
:namea parameter to capture from the route up to /, ?, or end of string
*splata splat to capture from the route up to ? or end of string
()Optional group that doesn't have to be part of the query. Can contain nested optional groups, params, and splats
anything elsefree form literals

Some examples:

  • /some/(optional/):thing
  • /users/:id/comments/:comment
  • /*a/foo/*b
  • /books/*section/:title
  • /books?author=:author

The pattern matching for route paths is provided by route-parser.

Handlers

All subsequent parameters passed to a route definition function are handlers. There are four types of handlers. All are optional, though in most cases you'll declare both fromClient and fromServer handler for each route.

fromClient

The fromClient handler defines a function that runs on the server during SSR and the client during client-side navigation. It takes the following parameters:

ParameterTypeDescription
paramsObjectKey/value pairs consisting of the variables captured from the URL path and all query string parameters
requestObjectAn object containing path, method, headers and body

The fromClient handler returns an object to be applied to the app state. The shape of the object should match the AppModel class. Any keys present in the returned object will overwrite the corresponding keys in the current app state.

fromServer

The fromServer handler defines a function that only runs on the server. During client-side navigation, the router automatically sends an AJAX request to the server to run the route. The AJAX request URL is the same as the client-side URL with a .json suffix added.

The fromServer handler takes a string path to a module that exports a function that accepts the same parameters as fromClient:

ParameterTypeDescription
paramsObjectKey/value pairs consisting of the variables captured from the URL path and all query string parameters
requestObjectAn object containing path, method, headers and body
responseResponseAn object representing the http response. Defined in react-storefront/router/Response. Use this to set the response status and headers and send redirects.

The fromServer handler returns an object that will be shallow-merged into to the app's state tree using Object.assign. This means that if the app's state is:

{
  page: 'Product',
  product: null
}

... and the fromServer handler returns:

{
  product: {
    id: '1',
    name: 'Red Shirt'
  }
}

The resulting app state will be:

{
  page: 'Product',
  product: {
    id: '1',
    name: 'Red Shirt'
  }
}
Response Status

To set the response status, use response.status(code):

export default async function productHandler(params, request, response) {
  const product = await fetchProduct(params.id) // defined elsewhere

  if (product) {
    return product
  } else {
    response.status(404)
  }
}
Response Headers

To set response headers use response.set(name, value)

export default function productHandler(params, request, response) {
  response.set('x-my-custom-header', 'some value')
}
Redirects

To redirect the browser to another URL, use response.redirect(url, status)

export default function productHandler(params, request, response) {
  response.redirect('/', 302)
}

The default status is 301.

Sending a Verbatim Response

A fromServer handler can send a verbatim string response using response.send(body) rather than returning an object. For example:

// routes.js
router.get('/my-api',
  fromServer('./my-api-handler')
)
// my-api-handler.js
export default function myApiHandler(params, request, response) {
  response
    .set('content-type', response.JSON)
    .send(JSON.stringify({ foo: 'bar' }))
}

When response.send() is called in a fromServer handler, server-side rendering will not be performed.

cache

The cache handler specifies how long a response should be cached in Moovweb XDN's edge servers and whether or not it should be cached on the client by the service worker. One of the major benefits of React Storefront is that you don't need a separate service worker configuration. All caching, both client and server, can be configured via the router.

The cache handler takes an options argument that supports the following:

  • server
    • maxAgeSeconds - The number of seconds the result will be cached
    • key - A function that generates a custom key for server side caching. Two arguments are passed:
      • request - The request object with path, method, and headers
      • defaults - An object containing the key/value pairs used to create the default cache key. We generally recommend including these in your custom key. See the example below.
  • client - true to cache on the client, otherwise false

For example, to cache the page for 5 minutes on the server and on the client as well with the maxAgeSeconds set via router.configureClientCache(options):

cache({ 
  server: { 
    maxAgeSeconds: 300
  },
  client: true
})

To change the default client cache configuration, call: (defaults shown below)

router
  .configureClientCache({
    // the name for the runtime cache used by the service worker. 
    // A build number will be appended to ensure that the cache is 
    // removed when a new version of the app is deployed.
    cacheName: 'runtime' ,

    // The max number of entries to store in the cache
    maxEntries: 200,

    // The max TTL of each entry in seconds
    maxAgeSeconds: 3600
  })

The server cache is automatically cleared when a new version of the app is deployed. The cache can also be cleared from the Moov Console. The client cache is cleared as well, once the new version of the service worker is installed.

Defining a Custom Server Cache Key

You can define a custom key for server-side caching by specifying a key function in your server cache config. This is useful when you need to cache multiple responses for the same URL path. For example, to cache separate responses for desktop and mobile devices:

const UAParser = require('ua-parser-js')

new Router()
  .get('/', 
    cache({ 
      server: { 
        maxAgeSeconds: 300,
        key: (request, defaults) => ({
          ...defaults,
          mobile: ['iOS', 'Android'].includes(
            new UAParser(request.headers['user-agent']).getOS().name
          )
        })
      }
    })
    // fromClient and fromServer handlers ...
  )

Declaring a Fallback Route

You can handle all URLs that are not matched by a route using the fallback method. Here's an example that displays the homepage when a URL doesn't match any other route:

router.fallback(
  fromClient({ page: 'Home' }),
  fromServer('./home/home-handler')
)

The fallback method takes one or more route handlers just like other route methods.

Body Parsing

When the request's content-type header is application/json, multipart/form-data, or application/x-www-form-urlencoded, the value of request.body will be an object. All other content types result in a string.