Serverless React PWA on AWS

Create your brand

Give an identity to your app. Start with the name and provide a nice description. Once you give birth to it, choose also a theme color. Ah! You also need to create a logo. Notice that it should fit in a circle. Export the logo to a favicon.ico. You also need to export your logo to two square PNG images, with sizes 192 and 512 pixels respectively.

Homepage

Start with your index.html. Follows a working example, of course you are going to change title, author, description, colors, fonts, etc. In particular you need to provide a style tag with few instructions to render your Homepage as a standalone file, as well as some message in case JavaScript is disabled. Don't worry! Lighthouse will guide you. Something like the following is the bare minimum.

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">

    <title>Serverless React PWA on AWS</title>

    <meta name="author" content="Gianluca Casati">
    <meta name="description" content="How to create a serverless Progressive Web App, using React, Redux, React Router, TypeScript on frontend and AWS Lambda, Cloudfront, Amazon SES, API Gateway for a zero maintenance required backend.">

    <meta name="viewport" content="width=device-width, initial-scale=1">

    <meta name="theme-color" content="#eeeeee">
    <meta name="apple-mobile-web-app-capable" content="yes">
    <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">

    <link rel="manifest" href="manifest.json">

    <meta name="apple-mobile-web-app-title" content="React PWA">
    <meta name="application-name" content="React PWA">

    <style>
      html {
        color: #333333;
        font-family: "Lucida Console", Monaco, monospace;
      }

      body {
        background-color: #eeeeee;
      }

      noscript {
        color: magenta;
        text-align: center;
        font-size: 5vh;
      }
    </style>

    <link rel="stylesheet" href="style.css">
  </head>

  <body>
    <h1>Serverless React PWA on AWS</h1>

    <noscript>
      <p>Opsss... your JavaScript looks disabled 😒</p>
    </noscript>

    <script>
      if ('serviceWorker' in navigator) {
        window.addEventListener('load', function () {
          navigator.serviceWorker.register('cache.js', { scope: '/' })
        })
      }
    </script>
  </body>
</html>

Directory structure

This is the complete list of files I added to get a working Progressive Web App.

The manifest

Create a manifest.json file.

{
  "short_name": "React PWA",
  "name": "Serverless React PWA on AWS",
  "icons": [
    {
      "sizes": "192x192",
      "src": "/images/logo-192x192.png",
      "type": "image/png"
    },
    {
      "sizes": "512x512",
      "src": "/images/logo-512x512.png",
      "type": "image/png"
    }
  ],
  "start_url": "/",
  "display": "standalone",
  "background_color": "#333333",
  "scope": "/",
  "theme_color": "#333333"
}

Register a service worker

Here it is where the magic happens. The service worker is the technology that enables your app to use many Progressive Web App features, such as offline and add to homescreen. You already added the code to register a service worker at the bottom of your Homepage. You need also to add a cache.js file that implements a cache strategy.

const CACHE_NAME = 'cache-v0.0.0'
const CACHED_URLS = [
  '/',
  '/manifest.json',
  '/images/logo-192x192.png',
  '/images/logo-512x512.png',
  '/style.css'
]

// Open cache on install.
self.addEventListener('install', event => {
  event.waitUntil(async function () {
    const cache = await caches.open(CACHE_NAME)

    await cache.addAll(CACHED_URLS)
  }())
})

// Cache and update with stale-while-revalidate policy.
self.addEventListener('fetch', event => {
  const { request } = event

  if (request.cache === 'only-if-cached' && request.mode !== 'same-origin') {
    return
  }

  event.respondWith(async function () {
    const cache = await caches.open(CACHE_NAME)

    const cachedResponsePromise = await cache.match(request)
    const networkResponsePromise = fetch(request)

    if (request.url.startsWith(self.location.origin)) {
      event.waitUntil(async function () {
        const networkResponse = await networkResponsePromise

        await cache.put(request, networkResponse.clone())
      }())
    }

    return cachedResponsePromise || networkResponsePromise
  }())
})

// Clean up caches other than current.
self.addEventListener('activate', event => {
  event.waitUntil(async function () {
    const cacheNames = await caches.keys()

    await Promise.all(
      cacheNames.filter((cacheName) => {
        const deleteThisCache = cacheName !== CACHE_NAME

        return deleteThisCache
      }).map(cacheName => caches.delete(cacheName))
    )
  }())
})

Make your app installable

We are almost done! To make your app installable, another prerequisite is that it is served over HTTPS and that HTTP request are redirected to HTTPS. If you want to give it a try without buying a domain, you can use GitHub Pages. By the way, this webiste itself is a PWA and it is hosted in this repository: react-pwa/react-pwa.github.io.