React Storefront
|

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({
      edge: { 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({
  edge: {
    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
})
Clearing the cache

The edge cache is automatically cleared when a new version of the app is deployed. The cache can also be cleared from the Moovweb Control Center. The client cache is cleared as well, once the new version of the service worker is installed.

Defining a Custom Edge Cache Key

You can define a custom key for server-side caching by specifying a key property in your edge cache config. This is useful when you need to cache multiple variants of the same URL. For example, to cache separate responses for based on the user's currency preference stored in a cookie:

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

new Router().get(
  '/',
  cache({
    edge: {
      maxAgeSeconds: 300,
      key: createCustomCacheKey().addCookie('currency')
    }
  })
  // fromClient and fromServer handlers ...
)

fromOrigin(name)

The fromOrigin handler allows you to bypass Moovweb Cloud's JavaScript runtime entirely and proxy an origin directly from the edge. This is ideal for delivering pages or assets from a legacy site. In other words, a request handled by fromOrigin will take the following path: browser => edge => origin, instead of the usual path of browser => edge => js workers => origin.

get(
  '/assets/*path',
  cache({
    edge: {
      maxAgeSeconds: 60 * 60 * 24 * 365 // 1 year
    }
  }),
  fromOrigin('origin')
)

The name passed to fromOrigin(name) must correspond to the name of a "backend" in project's outer edge config in Moovweb Developer Center.

You can also transform the path if the URL at the origin differs from that of the PWA:

get('/some/source/path', fromOrigin('desktop').transformPath('/some/target/path'))

You can also transform paths with variables:

get('/catalog/:id', fromOrigin('desktop').transformPath('/products/{id}'))

redirectTo(path)

The redirectTo handler allows you to redirect the browser to a new location from the edge, without hitting the Moovweb JS runtime. This results in very fast redirections.

get('/some/source/path', redirectTo('/some/target/path'))

You can also redirect paths with variables:

get('/catalog/:id', redirectTo('/products/{id}'))

By default a response status of 302 will be used. You can override this using:

get('/some/source/path', redirectTo('/some/target/path').withStatus(301))

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.

Handling Errors

The Router provides an error handler that is called whenever an error is thrown from a route handler on either the client or the server:

new Router().error((error, params, request, response) => {
  response.status(500)

  return {
    page: 'Error',
    error: e.message,
    loading: false,
    stack: e.stack
  }
})

Like all other route handlers, the error handler returns data to be applied to the app's state tree. In the example above, we return a state object that will cause the app to hide the loading mask (if one is present) and display the error page. This matches the default functionality that is provided if you do not provide an error handler.

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.