This guide shows you how to serve a React application on Layer0. If you're using Next.js specifically, we suggest using the Next.js guide.

Layer0 only supports Node.js version 14 and higher

If you do not have Node.js installed on your system, download and install it from the official Node.js v14.x downloads page. Select the download that matches your operating system and run the installer. Note that the installer for Node.js will also install npm.

Note that while you can use any version of Node.js >= 14 locally, your app will run in Node 14 when deployed to the Layer0 cloud. Therefore we highly suggest using Node 14 for all development.

To prepare your React app for deployment on Layer0, install the Layer0 CLI globally:

npm install -g @layer0/cli

Then, in the root folder of your project, run:

layer0 init

This will automatically add all of the required dependencies and files to your project. These include:

  • The @layer0/core package - Allows you to declare routes and deploy your application on Layer0
  • The @layer0/prefetch package - Allows you to configure a service worker to prefetch and cache pages to improve browsing speed
  • The @layer0/react package - Provides a Prefetch component for prefetching pages
  • layer0.config.js - The main configuration file for Layer0.
  • routes.js - A default routes file that sends all requests to Next.js. Update this file to add caching or proxy some URLs to a different origin.
  • sw/service-worker.js A service worker implemented using Workbox.

React offers a great amount of flexibility in how you set up server side rendering. Frameworks like Next.js offer a standardized, built-in way of implementing SSR. If you're using Next.js specifically, we suggest using the Next.js guide. We'll assume at this point that you're not using Next.js, but have an existing Node app that is doing server-side rendering.

In order to render on Layer0, you need to provide a function that takes a node Request and Response and sends the HTML that results from the renderToString() method from react-dom/server. Configure that function using the server property of layer0.config.js. Here's an example:

// layer0.config.js

module.exports = {
  server: {
    path: 'layer0/server.js',
  },
}
// server.js - basic node example

const ReactDOMServer = require('react-dom/server')
const App = require('./app')

module.exports = function server(request, response) {
  const html = ReactDOMServer.renderToString(React.createElement(App, { url: request.url }))
  response.set('Content-Type', 'text/html')
  response.send(html)
}

If you already have an express app set up to do server side rendering, the server module can also export that instead:

// server.js - express example

const express = require('express')
const app = express()
const ReactDOMServer = require('react-dom/server')
const App = require('./app')

app.use((request, response, next) => {
  const html = ReactDOMServer.renderToString(React.createElement(App, { url: request.url }))
  response.set('Content-Type', 'text/html')
  response.send(html)
})

module.exports = app

We recommend bundling your server with Webpack. Your webpack config should use the following settings:

module.exports = {
  target: 'node',
  mode: 'production',
  output: {
    filename: '[name].js',
    path: path.resolve(__dirname, '..', 'dist'), // should match server.path in layer0.config.js
    libraryTarget: 'umd',
    libraryExport: 'default',
  },
  entry: {
    server: './layer0/server.js', // this should point to your server entry point, which should export a function of type (request: Request, response: Response) => void or an express app as the default export.
  },
}

Using the Router class from @layer0/core, you'll configure caching for each of your routes, and forward requests to the server module you configured in the previous section using the proxy function.

// routes.js

import { Router } from '@layer0/core/router'
import { BACKENDS } from '@layer0/core'

new Router()
  .get('/service-worker.js', ({ serviceWorker }) => {
    serviceWorker('dist/service-worker.js')
  })
  .get('/p/:id', ({ cache }) => {
    // cache product pages at the edge for 1 day
    cache({
      edge: {
        maxAgeSeconds: 60 * 60 * 24, // 1 day
      },
    })
  })
  .fallback(({ renderWithApp }) => {
    // send all requests to the server module configured in layer0.config.js
    renderWithApp()
  })

Add the Prefetch component from @layer0/react to your links to cache pages before the user clicks on them. Here's an example:

import { Link } from 'react-router'
import { Prefetch } from '@layer0/react'

export default function ProductListing() {
  return (
    <div>
      {/* ... */}
      {/* The URL you need to prefetch is the API call that the page component will make when it mounts. It will vary based on how you've implemented your site. */}
      <Prefetch url="/api/products/1.json">
        <Link to="/p/1">Product 1</Link>
      </Prefetch>
      {/* ... */}
    </div>
  )
}

By default, Prefetch waits until the link appears in the viewport before prefetching. You can prefetch immediately by setting the immediately prop:

<Prefetch url="/api/products/1.json" immediately>
  <Link to="/p/1">Product 1</Link>
</Prefetch>

In order for prefetching to work, you need to configure a service worker that uses the Prefetcher class from @layer0/prefetch. Here is an example service worker built using workbox:

// sw/service-worker.js

import { skipWaiting, clientsClaim } from 'workbox-core'
import { Prefetcher } from '@layer0/prefetch/sw'

skipWaiting()
clientsClaim()

new Prefetcher().route()

In order to install the service worker in the browser when your site loads, call the install function from @layer0/prefetch:

import { install } from '@layer0/prefetch/window'

install()

If you're building an app with create-react-app, you can use this router to get started:

// routes.js

const { Router } = require('@layer0/core/router')

const ONE_HOUR = 60 * 60
const ONE_DAY = 24 * ONE_HOUR
const ONE_YEAR = 365 * ONE_DAY

const edgeOnly = {
  browser: false,
  edge: { maxAgeSeconds: ONE_YEAR },
}

const edgeAndBrowser = {
  browser: { maxAgeSeconds: ONE_YEAR },
  edge: { maxAgeSeconds: ONE_YEAR },
}

module.exports = new Router()
  .prerender([{ path: '/' }])
  // js and css assets are hashed and can be far-future cached in the browser
  .get('/static/:path*', ({ cache, serveStatic }) => {
    cache(edgeAndBrowser)
    serveStatic('build/static/:path*')
  })
  // all paths that do not have a "." as well as "/"" should serve the app shell (index.html)
  .get('/:path*/:file([^\\.]+|)', ({ cache, appShell }) => {
    cache(edgeOnly)
    appShell('build/index.html')
  })
  // all other paths should be served from the build directory
  .get('/:path*', ({ cache, serveStatic }) => {
    cache(edgeOnly)
    serveStatic('build/:path*')
  })

Deploying requires an account on Layer0. Sign up here for free. Once you have an account, you can deploy to Layer0 by running the following in the root folder of your project:

layer0 deploy

For more on deploying, see Deploying.