Make a Progressive Web App with React
Tags: react, pwa
Published: 2021-03-01 23:48:00

## In this Guide

In this guide, we'll define what a PWA is and then we'll proceed to build one!

We'll go from definition to deployment using Create React App, Sanity, and Vercel. By the end, you'll be able to install the PWA on your phone and edit content for it on Sanity.

You can see the deployed app at [https://fill-in-the-blank.vercel.app/](https://fill-in-the-blank.vercel.app/) and see the final code here: [https://github.com/ricokahler/fill-in-the-blank](https://github.com/ricokahler/fill-in-the-blank)

**Prerequisites:**

1. Use of the command line and a code editor.
2. Basic understanding of JavaScript, JSON, and Web Development

> **PROTIP**: There's a lot of information and tools used in this guide. If you have any questions, feel free to reach out in the [Sanity Community Slack](https://slack.sanity.io/).

## What’s a PWA?

PWA stands for Progressive Web App and it defines a type of app that lives on the web but has enhanced capabilities to make it feel like any other native app on your phone, tablet, or computer.

Often, the term can be confusing because it's used in many different contexts and for many different reasons making the line between interactive website and PWA hard to define.

### The truth about PWAs

The truth is that there are no concrete requirements that determine whether your web app is a progressive web app or a plain old website.

What powers the most distinct features of PWAs (for example, notifications) are sets of individual web APIs that are already built into the browser.

That's right, they're not part of some framework you have to download and install, they're APIs that come with your browser, ready for use by default.

Don't believe me? [Open up the devtools](https://developers.google.com/web/tools/chrome-devtools/open) and type this into the console:

```javascript
Notification.requestPermission()
  .then(() => new Notification('Hey 👋'));
```

Cool right?

So to simplify and draw _some_ lines, let's define a few “enhanced capabilities” we'll be implementing for our PWA, but know that these criteria aren’t the strict rule.

1. **Interactivity** — This isn't necessarily a requirement exclusive to PWAs but rather to apps in general. For anything to be an app, you must be able to interact with it and do some sort of work inside it. In terms of PWAs, this typically means creating a JavaScript-powered site (for example, with React).
2. **Offline mode** — Offline mode is PWAs claim to fame — simply put, the ability to use a website without the internet is kinda magical ✨.
3. **Push notifications** — In this case, this is a key characteristic of native apps making an appearance on the web. Receiving a push notification through your device's native notification system blurs the line between native and web distinguishing your web app from others.
4. **Add it to the home screen** — The same goes for this one. For mobile devices, the ability to add an app to the home screen of your phone _really_ blurs the line between native and web. Additionally, when web apps are added to the home screen, both Android and iOS present your app in fullscreen without any browser bars.

So to me, if your site can do all these, then there's no question about it — it's definitely a progressive web app, and it's what we'll be making in this article.

## What is Sanity?

Before we get into the tutorial, let’s first take a look at Sanity and why it’s useful in the context of PWAs.

Sanity is a **structured content platform** and a **backend-as-a-service**.

### A structured content platform

Structured content is content with a known data shape. When you have a known data shape you can assume that certain properties are available and build your UI around them.

Sanity is a structured content platform* because it specializes in creating, editing, and distributing structured content. Sanity gives you a way to define the structure of your content…

```javascript
// this is how you define schemas in sanity
export default {
  title: 'Blog Post',
  name: 'blogPost',
  type: 'document',
  fields: [
    {
      title: 'Title',
      name: 'name',
      type: 'string',
    },
    {
      title: 'Content',
      name: 'content',
      type: 'text',
    },
  ],
};
```

…and then creates a UI for you and your editing team to rapidly create structured content items.

![A screenshot of Sanity Studio](https://cdn.sanity.io/images/81pocpw8/production/7ece08274be8265489601fd43448e263cbc2a62e-1476x984.png?w=500&q=75&auto=format)

Lastly, Sanity helps you distribute this structured content to _any_ type of technology — including PWAs.

> **PROTIP**: *Sanity can also be considered a headless content management system or _headless CMS_. However, they prefer the term _structured content platform_ instead of headless CMS because Sanity is not limited to typical blog-like CMS use-cases. It can do much more — like power a PWA!

### A backend-as-a-service

Sanity is also a backend-as-a-service meaning that it hosts a service for your frontend to call so that you can request content directly from Sanity without having to write any backend code.

In this tutorial, we’ll be using Sanity to populate the data inside of the app.

Without further adieu, let's create a PWA!

## Fill in the Blank — a PWA driven by Sanity

> **PROTIP**: All of the finished code is available on [GitHub](https://github.com/ricokahler/fill-in-the-blank). 😎

### Step 1: Create a new React app

In this tutorial, we’ll be leveraging `create-react-app` — [the React recommended way to create single-page applications](https://reactjs.org/docs/create-a-new-react-app.html#create-react-app) and its [PWA template](https://create-react-app.dev/docs/making-a-progressive-web-app/).

To get started, create a react app with the following line:

```sh
npx create-react-app fill-in-the-blank --template cra-template-pwa
```

![Create react app install GIF](https://cdn.sanity.io/images/81pocpw8/production/e1726e6fc931ca00740c99ac6aae038160747732-800x600.gif?w=500&q=75&auto=format)

Then open up the newly created project folder in your favorite editor, it should look like this.

![Screenshot of editor after create-react-app install](https://cdn.sanity.io/images/81pocpw8/production/b3fd08cfb1756c954bb6beebe1016c205c3d54e2-2050x1538.png?w=500&q=75&auto=format)

### Step 2: Create a new Sanity project

Next, we'll create a new Sanity project inside of this folder.

First, install the Sanity CLI:

```sh
npm i -g @sanity/cli
```

Then login with:

```sh
sanity login
```

![screenshot of sanity CLI and login](https://cdn.sanity.io/images/81pocpw8/production/fb4f55dbf38e8eebf2a9a7a2f858e2358eb647be-982x732.png?w=500&q=75&auto=format)

Then run:

```sh
sanity init
```

This will guide you on creating a new sanity project in the current folder.

For this, choose:

1. Create new project,  
2. "Fill in Blank" for the project name,  
3. The default dataset configuration (public, production),  
4. **Project output path `./sanity`**, and  
5. Clean Project template

![Screenshot of sanity CLI after sanity init](https://cdn.sanity.io/images/81pocpw8/production/1d38d4a19a3d7f5d444c3c43a51938a218475350-982x1152.png?w=500&q=75&auto=format)

This should create a `./sanity` folder in the current project folder.

![Screenshot of VSCode after sanity init](https://cdn.sanity.io/images/81pocpw8/production/a5cf350e478ef8caf2e5336dae11a4013320b1df-2050x1538.png?w=500&q=75&auto=format)

### Step 3: Create the mad lib schema

Up next we need to define the shape of our data. We'll do this by defining a schema in the `/sanity/schemas` folder.

Create the file `/sanity/madLib.js` and paste in the following:

```javascript
// /sanity/madLib.js
const madLib = {
  name: 'madLib',
  title: 'Mad Lib',
  type: 'document',
  fields: [
    {
      title: 'Title',
      name: 'title',
      type: 'string',
      validation: (Rule) => Rule.required(),
    },
    {
      name: 'slug',
      type: 'slug',
      options: { source: 'title' },
      validation: (Rule) => Rule.required(),
    },
    {
      title: 'Image',
      name: 'image',
      type: 'image',
      validation: (Rule) => Rule.required(),
    },
    {
      name: 'story',
      type: 'array',
      of: [
        {
          type: 'block',
          of: [
            // this defines is in an inline object that can be embedded within
            // each line of text in the story.
            // https://www.sanity.io/docs/block-type#of-d0f97ffa1dd9
            {
              title: 'Placeholder',
              name: 'placeholder',
              type: 'object',
              fields: [
                {
                  title: 'Type',
                  description:
                    'The type of placeholder. E.g. Adjective, Noun, Celebrity, Animal',
                  name: 'type',
                  type: 'string',
                },
              ],
            },
          ],
        },
      ],
      validation: (Rule) => Rule.required(),
    },
  ],
};

export default madLib;
```

This defines the shape of our data and allows Sanity to generate forms using this shape. The `story` field utilizes [inline blocks](https://www.sanity.io/docs/block-type#of-d0f97ffa1dd9) to allow for placeholders for our mad libs.

![GIF demo of inline block in sanity studio](https://cdn.sanity.io/images/81pocpw8/production/afc508017659fbcb9b541b9a847a17bff9adf11f-800x396.gif?w=500&q=75&auto=format)

To make Sanity aware of this shape, import it into `/sanity/schema.js` like so:

```javascript
// /sanity/schema.js
import createSchema from 'part:@sanity/base/schema-creator';
import schemaTypes from 'all:part:@sanity/base/schema-type';
import madLib from './madLib';

export default createSchema({
  name: 'default',
  types: schemaTypes.concat([madLib]),
});
```

### Step 4: Deploy the Studio, Enter some data

Now that we have a schema, we can deploy and populate our Sanity instance with data.

`cd` into the sanity folder and run `sanity deploy`, to deploy the current Sanity studio to the web.

```sh
cd ./sanity
sanity deploy
```

Feel free to pick any hostname you'd like.

![Screenshot of the Sanity CLI after a deploy](https://cdn.sanity.io/images/81pocpw8/production/d5df88ca4ffad66b3fc974066736b1760c26de7b-982x732.png?w=500&q=75&auto=format)

After it's done, you can go to that URL, log in, and see your very own Sanity Studio instance.

![Screenshot of the deployed Sanity Studio](https://cdn.sanity.io/images/81pocpw8/production/59c0d3d0516fe75b86e86097fa4d3415c7418f4c-2482x1842.png?w=500&q=75&auto=format)

Go ahead and create a sample Mad Lib. We'll need it for the next section.

> **PROTIP**: See the [official documentation](https://www.sanity.io/docs/sanity-studio) for more info on Sanity Studio

### Step 5: Setting up our React app

Now we focus on building the app and utilizing the structured content Sanity provides.

In this project, we'll be using the following libraries:

1. [`@sanity/block-content-to-react`](https://github.com/sanity-io/block-content-to-react) — the lib used to render [Sanity's block editor](https://www.sanity.io/docs/block-content) to React components.  
2. [`@sanity/image-url`](https://www.sanity.io/docs/image-url) — the lib used to generate image URLs.  
3. [`@sanity/client`](https://www.sanity.io/docs/js-client) — the client used to query data from the Sanity's backend.  
4. [`react-query`](https://react-query.tanstack.com/) — a server state management library for asynchronous data fetches. Very useful when paired with the `@sanity/client`.  
5. [`react-router-dom`](https://reactrouter.com/web/guides/quick-start) — a client-side routing library to allow us to navigate within our app.

To install these (and a few others), run the following command at the root of your project folder

```sh
yarn add @sanity/block-content-to-react @sanity/image-url @sanity/client react-query react-router-dom modern-css-reset
```

![Screenshot of yarn installation of deps](https://cdn.sanity.io/images/81pocpw8/production/b446bef5f5b132367b383237dc2ab767f38706bd-800x595.gif?w=500&q=75&auto=format)

To finish the installation, update your `index.js` with the following:

```jsx
// /src/index.js
import 'modern-css-reset';
import './index.css';
import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter } from 'react-router-dom';
import { QueryClient, QueryClientProvider } from 'react-query';
import { persistWithLocalStorage } from 'react-query/persist-localstorage-experimental';
import App from './App';
import * as serviceWorkerRegistration from './serviceWorkerRegistration';
import reportWebVitals from './reportWebVitals';

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      cacheTime: 1000 * 60 * 60 * 24, // 24 hours
    },
  },
});

persistWithLocalStorage(queryClient);

ReactDOM.render(
  <React.StrictMode>
    <QueryClientProvider client={queryClient}>
      <BrowserRouter>
        <App />
      </BrowserRouter>
    </QueryClientProvider>
  </React.StrictMode>,
  document.getElementById('root'),
);

serviceWorkerRegistration.register();

reportWebVitals();

```

This will:

- include a simple [CSS reset](https://stackoverflow.com/q/11578819/5776910) to help with styling  
- `register` [the create-react-app service worker](https://create-react-app.dev/docs/making-a-progressive-web-app/) to enable offline mode  
- install/set up `react-router`  
- install/set up `react-query` with the [persist-localstorage plugin](https://react-query.tanstack.com/plugins/persist-localstorage). This plugin pairs well with the create-react-app service worker for offline mode

Now we can start the local server and see our changes live:

```sh
yarn start
```



![Screenshot of local server started](https://cdn.sanity.io/images/81pocpw8/production/3f6abb489537015b828e989082b8bcd125443caa-982x744.png?w=500&q=75&auto=format)

### Step 6: Routing and component architecture

The next file, we'll be tackling is `App.js`. After `index.js`, this is the primary file that will hold our app's main structure.

In this app, we'll have 3 types of routes:

* `/` — The main index t  hat will list all of the available mad libs for our users to complete
* `/mad-libs/:slug` — The individual page containing one mad lib. This page will allow our users to fill in the blanks and will present the finished story once filled in.
* A 404 page

We'll be implementing this using react-router and the [`Switch`](https://reactrouter.com/web/api/Switch) and [`Route`](https://reactrouter.com/web/api/Route) components.

Create the following files:

* `/src/MadLibList.js`
* `/src/MabLib.js`

And paste in this implementation for both for now. We'll fill it in later.

```jsx
import React from 'react';

const WorkInProgress = () => <>Work in progress</>;

export default WorkInProgress;

```

Copy the following into `/src/NotFound.js`:

```jsx
// /src/NotFound.js
function NotFound() {
  return (
    <div>
      <h1>We're Sorry</h1>
      <p>We couldn't find the page you were looking for.</p>
    </div>
  );
}

export default NotFound;
```

And lastly, copy the following in `App.js` and `App.module.css`

```jsx
// /src/App.js
import { Route, Switch, Link } from 'react-router-dom';
import MadLibList from './MadLibList';
import MadLib from './MadLib';
import NotFound from './NotFound';
import styles from './App.module.css';

function App() {
  return (
    <>
      <header className={styles.header}>
        <Link className={styles.headerLink} to="/">
          Sanity Mad Libs
        </Link>
      </header>

      <main className={styles.main}>
        <div className={styles.container}>
          <Switch>
            <Route component={MadLibList} path="/" exact />
            <Route component={MadLib} path="/mad-libs/:slug" />
            <Route component={NotFound} />
          </Switch>
        </div>
      </main>
    </>
  );
}

export default App;
```

```css
/* /src/App.module.css */
.header {
  padding: 1rem;
  box-shadow: 0 0 24px 0 rgba(0, 0, 0, 0.16);
  font-weight: bold;
  position: sticky;
  background-color: white;
  top: 0;
}

.headerLink {
  color: black;
  text-decoration: none;
}

.headerLink:hover {
  text-decoration: underline;
}

.main {
  padding: 1rem;
  overflow: auto;
}

.container {
  margin: 0 auto;
  max-width: 100%;
  width: 425px;
}
```

> **PROTIP**: We're using CSS modules to style here because it isolates CSS to the current component and it's [supported by default in create-react-app](https://create-react-app.dev/docs/adding-a-css-modules-stylesheet/).

Your project should look like this afterward:

![Screenshot of the code editor and filesystem after this step.](https://cdn.sanity.io/images/81pocpw8/production/ff4e865b9e98bedf4a844241f194e51b276e903f-2328x1526.png?w=500&q=75&auto=format)

![Screenshot of the app on the local server after this step](https://cdn.sanity.io/images/81pocpw8/production/95d91eccb55bf5f3f733b24b76b67731b6decabd-2482x1842.png?w=500&q=75&auto=format)

### Step 7: Add a new CORS origin to Sanity and set up the sanity client

Before we start pulling data from Sanity, we need to tell Sanity that it's okay for our app to be accessing data from the [origin](https://developer.mozilla.org/en-US/docs/Glossary/Origin) `http://localhost:3000`.

If we don't do this, our network requests will fail with [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) errors.

Go to [sanity.io/teams](https://sanity.io/teams) > "Fill in the Blank" > Settings > API > CORS Origins

From here add `http://localhost:3000` to the allowed origins.

![Screenshot of sanity.io/teams where you can add a new CORS origin.](https://cdn.sanity.io/images/81pocpw8/production/f7c93c479a5b2ed2b6a04f92f020a33f603478a7-2356x1182.png?w=500&q=75&auto=format)

Now we can set up the sanity client to be used for queries in our app.

Create the sanity client and sanity image builder in `/src/sanity.js`

```javascript
// /src/sanity.js
import SanityClient from '@sanity/client';
import ImageUrlBuilder from '@sanity/image-url';

export const sanity = SanityClient({
  projectId: 'your-project-id',
  dataset: 'production',
  // this option enables faster responses
  // but can return stale data at times.
  // recommended for client-side queries
  useCdn: true,
});

export const imageUrlBuilder = ImageUrlBuilder(sanity);
```

Be sure to replace the `'your-project-id'` with your project ID found in `/sanity/sanity.json`.

### Step 8: Query Sanity for a summarized list of items

Now we're finally ready to query some data from Sanity.

In this step, we'll be implementing a list of summarized data.

We'll be querying for all documents in our database, however, instead of requesting the whole document, we'll only be requesting the `title`, `image`, and `slug` fields. This makes it so that we're only querying for what we need on the page.

When we write the individual mab lib page, that's when we'll pull everything.

Paste the following into `/src/MadLibList.js` and `/src/MadLibList.module.css`

```jsx
// /src/MadLibList.js
import { useQuery } from 'react-query';
import { Link } from 'react-router-dom';
import { sanity, imageUrlBuilder } from './sanity';
import styles from './MadLibList.module.css';

const query = `
  *[ _type == 'madLib' ] { title, image, slug }
`;

function MadLibList() {
  // in this one line, data is fetched from sanity via the sanity client and
  // stored into application state via react-query!
  const { data: madLibs } = useQuery('madLibsList', () => sanity.fetch(query));

  // if we don't have madLibs yet, then the data must be loading
  if (!madLibs) {
    return <h1>Loading…</h1>;
  }

  return (
    <>
      <h1>Mad Libs</h1>

      <ul className={styles.list}>
        {/* loop through all of the mabLib and show them in a list */}
        {madLibs.map(({ title, slug, image }) => (
          <li key={slug.current}>
            {/* ;ink comes from react-router and it powers navigation on the */}
            {/* site. here we use sanity slugs to create unique URLs. */}
            <Link className={styles.tile} to={`/mad-libs/${slug.current}`}>
              <img
                alt={title}
                // use the sanity `imageUrlBuilder` to
                // generate optimized images on the fly
                src={imageUrlBuilder.width(425).height(425).image(image).url()}
                width="425px"
                height="425px"
              />
              <h2 className={styles.tileTitle}>{title}</h2>
            </Link>
          </li>
        ))}
      </ul>
    </>
  );
}

export default MadLibList;
```

```css
/* /src/MadLibList.module.css */
.list {
  list-style-type: none;
  padding: 0;
}

.list > *:not(:last-child) {
  margin-bottom: 1rem;
}

.tile {
  position: relative;
  display: block;
}

.tile::after {
  position: absolute;
  top: 0;
  bottom: 0;
  right: 0;
  left: 0;
  content: ' ';
  background: linear-gradient(transparent, black);
}

.tileTitle {
  position: absolute;
  bottom: 1rem;
  left: 1rem;
  z-index: 1;
  color: white;
}

.tile:hover .tileTitle {
  text-decoration: underline;
}
```

There are a few things going on in this file:

1. We're using [`react-query`](https://react-query.tanstack.com/) to manage the fetching and cache of data from Sanity. It utilizes the [`@sanity/client`](https://www.npmjs.com/package/@sanity/client) to get the data then saves it in memory. When this process is finished, it populates the `madLibs` variable. See the [`react-query` docs](https://react-query.tanstack.com/quick-start) for more info.
2. After the data is ready, we map over each item and return a clickable link. Each link renders an optimized image with the [`imageUrlBuilder`](https://www.sanity.io/docs/image-url).
3. Links and navigation are powered by [`react-router`](https://reactrouter.com/web/guides/quick-start). `react-router`'s `<Link>` accepts a `to` prop which we populate to match the route we created back in step 6. We utilize [slugs in Sanity](https://www.sanity.io/docs/slug-type) to create unique URLs.

When you're finished, your app should look like this:

![Screenshot of the app after this step.](https://cdn.sanity.io/images/81pocpw8/production/653e0dc42668fccfb51ab7e1efc6640342d1f2ad-2030x1374.png?w=500&q=75&auto=format)

### Step 9: Create the full detail pages

Next, we'll create the detail pages by populating `MabLib.js`.

Paste the following into `/src/MadLib.js` and `/src/MadLib.module.css`:

```jsx
// /src/MadLib.js
import { useQuery } from 'react-query';
import { useParams, Link } from 'react-router-dom';
import { useState, useEffect } from 'react';
import BlockContent from '@sanity/block-content-to-react';
import { sanity, imageUrlBuilder } from './sanity';
import styles from './MadLib.module.css';

const query = `
  *[ _type == 'madLib' && slug.current == $slug ]
`;

function MadLib() {
  // this variable is populated from `react-router` which pulls it from the URL
  const { slug } = useParams();

  // data is fetched from sanity via the sanity client and stored into
  // application state via react-query. note that the slug is used as the
  // "query key": https://react-query.tanstack.com/guides/query-keys
  const { data = [] } = useQuery(slug, () => sanity.fetch(query, { slug }));

  // we'll use destructuring assignment to return the first mab lib
  const [madLib] = data;

  // this will store the state of the answers of this mad lib
  const [answers, setAnswers] = useState(
    // if the items exist in localStorage, then
    localStorage.getItem(slug)
      ? // then set the initial state to that value
        JSON.parse(localStorage.getItem(slug))
      : // otherwise, set the initial state to an empty object
        {},
  );

  // this is a react "effect" hook: https://reactjs.org/docs/hooks-effect.html
  // we use this to watch for changes in the `slug` or `answers` variables and
  // update local storage when those change.
  useEffect(() => {
    localStorage.setItem(slug, JSON.stringify(answers));
  }, [slug, answers]);

  if (!madLib) {
    return <h1>Loading…</h1>;
  }

  // once the mad lib is loaded, we can map through the structured content to
  // find our placeholder shape. the end result is an array of these placeholders
  const placeholders = madLib?.story
    .map((block) => block.children.filter((n) => n._type === 'placeholder'))
    .flat();

  // using the above placeholders array, we calculate whether or not all the
  // blanks are filled in by checking the whether every placeholder has a value
  // in the `answers` state variable.
  const allBlanksFilledIn = placeholders?.every(
    (placeholder) => answers[placeholder._key],
  );

  return (
    <>
      <h2 className={styles.title}>{madLib.title}</h2>
      <img
        className={styles.img}
        alt={madLib.title}
        src={imageUrlBuilder.width(425).height(425).image(madLib.image).url()}
      />
      {!allBlanksFilledIn ? (
        // if all the blanks are _not_ filled in, then we can show the form
        <>
          <p>Fill in the blank!</p>
          <p>When you're done, the finished mad lib will appear.</p>
          <form
            // this `onSubmit` will fire when the user clicks the submit button
            onSubmit={(e) => {
              e.preventDefault();

              const answerEntries = Array.from(
                // find all the inputs
                e.currentTarget.querySelectorAll('input'),
              )
                // then get the name and values in a tuple
                .map((inputEl) => [inputEl.name, inputEl.value]);

              // use `Object.fromEntries` to transform them back to an object
              const nextAnswers = Object.fromEntries(answerEntries);

              setAnswers(nextAnswers);
            }}
          >
            <ul className={styles.list}>
              {/* for each placeholder… */}
              {placeholders.map(({ _key, type }) => (
                <li className={styles.placeholder} key={_key}>
                  {/* …render an input an a label. */}
                  <input
                    // the `name` of the input will be the sanity `_key`:
                    // https://www.sanity.io/docs/array-type#why-the-key-92296c6c45ea
                    // this will enables us to match this input value with the
                    // correct placeholder
                    name={_key}
                    className={styles.input}
                    id={_key}
                  />
                  <label className={styles.label} htmlFor={_key}>
                    {type}
                  </label>
                </li>
              ))}
            </ul>
            <button className={styles.button}>Submit!</button>
          </form>
        </>
      ) : (
        // if all the blanks are filled in, then we can show the rendered
        // story with a custom serializer for the type `placeholder`
        <>
          <BlockContent
            className={styles.blockContent}
            blocks={madLib.story}
            serializers={{
              // see here: https://github.com/sanity-io/block-content-to-react
              types: { placeholder: ({ node: { _key } }) => answers[_key] },
            }}
          />

          <button
            className={styles.button}
            onClick={() => {
              // we reset the state on click after the users confirms it's okay.
              if (window.confirm('Are you sure you want to reset?')) {
                setAnswers({});
              }
            }}
          >
            Reset
          </button>

          {/* this is a simple link back to the main mab libs index */}
          <Link className={styles.button} to="/">
            ← More Mad Libs
          </Link>
        </>
      )}
    </>
  );
}

export default MadLib;
```

```css
/* /src/MadLib.module.css */
.list {
  list-style-type: none;
  padding: 0;
}

.title {
  text-align: center;
}

.img {
  margin-bottom: 1rem;
}

.placeholder {
  padding: 1rem;
  display: flex;
  flex-direction: column;
  align-items: center;
}

.input {
  border: none;
  border-bottom: 1px solid black;
  outline: none;
  padding: 0.5rem;
  width: 300px;
  max-width: 100%;
  text-align: center;
}

.input:focus {
  border-bottom: 3px solid black;
}

.label {
  text-transform: uppercase;
  font-size: 12px;
  font-weight: bold;
}

.button {
  appearance: none;
  outline: none;
  background: none;
  padding: 0.5rem 1rem;
  border: 1px solid black;
  width: 100%;
  cursor: pointer;
  transition: all 250ms;
  display: block;
  margin-bottom: 1rem;
  text-align: center;
  text-decoration: none;
  color: black;
}

.button:focus,
.button:hover {
  background-color: #eee;
}

.button:active {
  background-color: #ddd;
}

.blockContent > p {
  margin-bottom: 1rem;
}
```

This step is where many things come together and get fancy.

In this step we:

1. Used [`react-router`](https://reactrouter.com/web/api/Hooks/useparams) to get the slug from the URL via the [`useParams`](https://reactrouter.com/web/api/Hooks/useparams) hook.
2. Used [`react-query`](https://react-query.tanstack.com/) along with the [`@sanity/client`](https://www.sanity.io/docs/js-client) to query for the full mad lib document matching the given slug.
3. This is paired with React's [`useState hook`](https://reactjs.org/docs/hooks-state.html), allowing us to react to work the user does on this page (like fill in blanks).
4. Lastly, we save the state into [`localStorage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage) for offline use.

When you're done, the app should look like this:

![Screenshot of the app after this step.](https://cdn.sanity.io/images/81pocpw8/production/d607b41703f682909807502211ea145d8b5698cd-2030x1374.png?w=500&q=75&auto=format)

![Screenshot of the app after this step.](https://cdn.sanity.io/images/81pocpw8/production/5075a838e84ce0cd478a1456372139f33d81d7a0-2030x1374.png?w=500&q=75&auto=format)

> **PROTIP**: 👋 There's a lot to unpack in this step. Feel free to take the time and read the comments!

### Step 10: Adding notifications

As promised, let's add some notifications.

For this we'll be using the [Notifications API](https://developer.mozilla.org/en-US/docs/Web/API/Notifications_API/Using_the_Notifications_API) we discussed earlier thats available natively in web browsers.

Copy the following code into `setupNotifications.js`:

```javascript
// /src/setupNotifications.js
import { sanity } from './sanity';

async function setupNotifications() {
  // Check if the browser supports notifications
  if (!('Notification' in window)) {
    // early return if not
    return;
  }

  // Check if the user denied notifications
  if (Notification.permission === 'denied') {
    // early return if so
    return;
  }

  // Wait for the user to give notification permissions
  await new Promise(async (resolve) => {
    if (Notification.permission === 'granted') {
      resolve();
    }

    const result = await Notification.requestPermission();

    if (result === 'granted') {
      resolve();
    }
  });

  const getMabLibCount = async () =>
    sanity.fetch(`count(*[ _type == 'madLib' ])`);

  let previous = await getMabLibCount();
  // setup interval to poll for new mad libs every minute
  setInterval(async () => {
    const current = await getMabLibCount();
    // the the current count is greater than the previous, send a notification
    if (current > previous) {
      previous = current;
      new Notification("There's a new Mad Lib for you to try!");
    }
  }, 60 * 1000);
}

export default setupNotifications;
```

Then in `index.js`, import and call `setupNotifications.js`

```jsx
// /src/index.js
import 'modern-css-reset';
import './index.css';
import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter } from 'react-router-dom';
import { QueryClient, QueryClientProvider } from 'react-query';
import { persistWithLocalStorage } from 'react-query/persist-localstorage-experimental';
import App from './App';
import * as serviceWorkerRegistration from './serviceWorkerRegistration';
import reportWebVitals from './reportWebVitals';
// NEW 👇👇👇
import setupNotifications from './setupNotifications';
//     👆👆👆

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      cacheTime: 1000 * 60 * 60 * 24, // 24 hours
    },
  },
});

persistWithLocalStorage(queryClient);

ReactDOM.render(
  <React.StrictMode>
    <QueryClientProvider client={queryClient}>
      <BrowserRouter>
        <App />
      </BrowserRouter>
    </QueryClientProvider>
  </React.StrictMode>,
  document.getElementById('root'),
);

serviceWorkerRegistration.register();

reportWebVitals();

// NEW 👇👇👇
setupNotifications();
//     👆👆👆
```

This code will setup an interval to poll for new entries in the database and will show a notifications if the count of `madLibs` increases while the app is open.

> **GOTCHA**: This code only takes advantage of the Notifications API but does not utilize background sync. This means the notifications are only sent when the app is open.

There are additional web APIs required in order to make this work while the app isn't open that are not in the scope of this article.

### Step 11: Updating default template and the manifest.json

This is the last bit of code we need update in order to finish our PWA 🎉.

All we need to do is update `/public/index.html` and `/public/manifest.json`. This is actually all we need to do to get the "Add it to the home screen" functionality working.

```json
{
  "short_name": "Fill in the Blank",
  "name": "Fill in the Blank",
  "icons": [
    {
      "src": "favicon.ico",
      "sizes": "64x64 32x32 24x24 16x16",
      "type": "image/x-icon"
    },
    {
      "src": "logo192.png",
      "type": "image/png",
      "sizes": "192x192"
    },
    {
      "src": "logo512.png",
      "type": "image/png",
      "sizes": "512x512"
    }
  ],
  "start_url": ".",
  "display": "standalone",
  "theme_color": "#000000",
  "background_color": "#ffffff"
}
```

```html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <meta name="theme-color" content="#000000" />
    <meta
      name="description"
      content="Web site created using create-react-app"
    />
    <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
    <!--
      manifest.json provides metadata used when your web app is installed on a
      user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
    -->
    <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
    <!--
      Notice the use of %PUBLIC_URL% in the tags above.
      It will be replaced with the URL of the `public` folder during the build.
      Only files inside the `public` folder can be referenced from the HTML.

      Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
      work correctly both with client-side routing and a non-root public URL.
      Learn how to configure a non-root public URL by running `npm run build`.
    -->
    <title>Fill in the Blank</title>
  </head>
  <body>
    <noscript>You need to enable JavaScript to run this app.</noscript>
    <div id="root"></div>
    <!--
      This HTML file is a template.
      If you open it directly in the browser, you will see an empty page.

      You can add webfonts, meta tags, or analytics to this file.
      The build step will place the bundled scripts into the <body> tag.

      To begin the development, run `npm start` or `yarn start`.
      To create a production bundle, use `npm run build` or `yarn build`.
    -->
  </body>
</html>
```

> **PROTIP**: Feel free to customize any colors or images in any of these files!

### Step 12: Deploying to Vercel

Home-stretch, let's deploy this thing to the web!

In order to do that, we'll use the provider [Vercel](https://vercel.com).

Vercel is a cloud platform for static web sites and cloud functions. It offers free and fast hosting for the kind of site we just made.

> **PROTIP**: [Netlify](https://www.netlify.com/) is also a great alternative to Vercel. You can't go wrong with either!

In order to deploy to Vercel, first, we'll commit all of the changes we've been making in our repo.

Run the following:

```sh
git checkout -b main # create a main branch
git add -A # stage all files for this commit
git commit -m "first commit" # commit the changes
```

Now we're ready to push our changes up to GitHub.

Login into GitHub and then create a **New repository**.

![Screenshot of GitHub's "New repository" button](https://cdn.sanity.io/images/81pocpw8/production/a7e74fe41a35bdf9e05a7cff8869e5fd26bf59fc-1488x568.png?w=500&q=75&auto=format)

Then fill in the following:

![Screenshot of the GitHub create repository UI. Select "public" and leave the rest of the options blank.](https://cdn.sanity.io/images/81pocpw8/production/af0812c350db5a07b4fbeab07f19fbd0b98acc96-1990x1788.png?w=500&q=75&auto=format)

Now we have a "[remote](https://git-scm.com/book/en/v2/Git-Basics-Working-with-Remotes)" we can push to. Let's push our code up with the following command:

```sh
git remote add origin https://github.com/ricokahler/fill-in-the-blank.git
git branch -M main
git push -u origin main
```

The code should now show up on GitHub!

![Screenshot of GitHub](https://cdn.sanity.io/images/81pocpw8/production/b884254f16f1ba222a216fcbdbd31381c1353634-2424x1328.png?w=500&q=75&auto=format)

Now we can log in to Vercel and deploy!

Go to [`vercel.com`](https://vercel.com) and click "Sign Up" and then "Continue with GitHub".

After a successful sign up, you should be taken to the [New app page](https://vercel.com/new).

From here, click "Add GitHub Org or Account" and then allow Vercel access to your repositories.

![Screenshot of Vercel New "Add GitHub Org or Account"](https://cdn.sanity.io/images/81pocpw8/production/b7c710ef4670db6ef621d5e962ae2990a749920b-1108x1186.png?w=500&q=75&auto=format)

After you've done that you can import the git repo we just created.

![Screenshot of Vercel "Import GIt Repository"](https://cdn.sanity.io/images/81pocpw8/production/9c8f090d569df409bd5d99fdcd1ee9a485c17d1e-1472x520.png?w=500&q=75&auto=format)

From here, select your personal account, and then, simply "Deploy"

![GIF of deploy from Vercel](https://cdn.sanity.io/images/81pocpw8/production/a8419eb43fd3e5513732c4c414d21f2111b28909-800x378.gif?w=500&q=75&auto=format)

And that's all there is to the deployment!

**There's one last thing to do.**

Visit the site, and then copy the URL. We'll need to add it to the allowed CORS origins like in Step 7.

![Screenshot of sanity.io/teams to add the CORS origin](https://cdn.sanity.io/images/81pocpw8/production/0e97f2664b8de3090f360dda09b98aa4cf87267a-2390x1020.png?w=500&q=75&auto=format)

Now the site should load correctly.

To test "add it to the home screen", go to the site on your phone.

On iPhone, click the share button, and then "Add to Home Screen".



![Screenshot of "Add to Home Screen" in iOS Safari](https://cdn.sanity.io/images/81pocpw8/production/4da2b7e3558caabf4cebcbae37b62eecc32de795-1125x2436.jpg?w=500&q=75&auto=format)



![Screenshot of App on the Home Screen](https://cdn.sanity.io/images/81pocpw8/production/b1ae66a09492347255535b64437f90c6c4115a2e-1125x2436.png?w=500&q=75&auto=format)

Notice that when you open this app from the home screen, it appears as a full screen app. Furthermore, this app also works offline after the initial load. Try it by going in airplane mode!

![Fireworks](https://cdn.sanity.io/images/81pocpw8/production/6130ff5f3827a92d88f828cc8a6d9daebb862db1-6000x4000.jpg?w=500&q=75&auto=format)

**That's it! Congrats, you made your first PWA with React and Sanity.**