Progressive Web Apps with React & Preact.js

Progressive Web Apps (PWA) have come onto the scene in a big way and with more mobile browsers supporting them, they might very well be the future of web applications. A PWA is actually pretty simple, and virtually any website or application can be converted into a baseline Progressive Web App. Here is a checklist from Google on all the things a website needs in order to be considered a PWA. Basically if you have a website or web application that loads fast, is served over HTTPS, and is responsive for mobile and tablet devices, then you are already most of the way there. You'll just need a manifest.json file and a Service Worker JavaScript file.

The manifest.json file tells a compatible browser that you have a Progressive Web App and, on certain mobile devices, it will trigger a prompt to add the app to the Home Screen. It also includes information about the icons that are displayed once added to the Home Screen and the name of the application.

A service worker is a bit more complex, but it's also very powerful. It's essentially a JavaScript file that runs in the background and listens for events, even if the website or application is closed. It can do things like cache the website's files for offline access, enable push notifications, and even access the camera of a mobile device. This is what give your PWA superpowers and makes it feel like a native mobile application.

Progressive Web Apps with React.js and Create React App

If you use React.js and Create React App (CRA) to start your projects, the good news is that the resulting app will be a Progressive Web App by default. Create React App already has everything needed, including the manifest.json ( in the /public directory ) and a Service Worker file called service-worker.js ( handled by registerServiceWorker.js ). You'll have to run yarn build or npm run build and then serve the /build folder before seeing them in action.

React Service Worker

With the manifest.json, you'll need to generate and add the appropriate icons for the wide variety of devices. A great resource for generating a proper manifest.json file is app-manifest.firebaseapp.com.

The Service Worker provided by CRA will provide support for offline mode. This means that it will cache all the files generated by CRA during the build process and store them in the browser's Cache Storage. If you turn off your internet connection. the application will still load!

React App Cache

This is, however, the bare minimum required for a PWA. The default service-worker.js won't cache any external data or resources and it won't have neat features such as push notifications. You'll probably want to register a custom Service Worker if you wish to get the most out your PWA.

PWAs with Preact.js, Custom Service Workers, and Workbox

While CRA is a fantastic tool, it doesn't do much for you in terms of code optimization. A Progressive Web App needs to be as fast and performant as possible, and this means smaller JavaScript bundles and code splitting. Enter Preact.js, which is a slimmer alternative to React that also has built in support for PWAs. The Preact CLI functions much like Create React App and it's just as easy to use.

The Service Worker that Preact CLI generates (called sw.js) will cache any generated files for offline use, but what if our web app uses an external API? If we fetch data or images from an external site, then those resources won't be cached by our default Service Worker. We'd need to register our own custom Service Worker to enable more robust PWA features.

Here is a wonderful article by Dave Hudson on how to implement a custom Service Worker into Preact.js. If you'd rather skip his post, I've made a repo of his finished product here for download. Also included in the repo is the latest version of Workbox, which is a set of libraries by Google for PWAs. Workbox makes writing our custom Service Worker much easier and exposes many advanced features for a PWA.

Building a News Feed PWA using Preact.js

Starting with the Preact-Workbox repo, we'll be creating a super simple Progressive Web App that pulls in the news using this News API. We will then add a few simple lines of code into our Service Worker to enable Workbox's offline caching features.

Preact.js News Feed PWA ( view source )

Let's kick things off by cloning the repo and running the dev environment.

git clone https://github.com/ChangoMan/Preact-Workbox.git preact-demo
cd preact-demo
npm install
npm run dev
# Navigate to http://localhost:8080 in your browser

Here you will see the default boilerplate for a Preact app created with the CLI. Let's add some code to make our very basic news feed app. Open up the main CSS file at src/style/index.css and replace with the following:

html,
body {
  height: 100%;
  width: 100%;
  padding: 0;
  margin: 0;
  background: #fafafa;
  font-family: "Helvetica Neue", arial, sans-serif;
  font-weight: 400;
  color: #444;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

* {
  box-sizing: border-box;
}

#app {
  height: 100%;
}

.site-header {
  padding: 2rem 0 1rem;
}

main {
  display: grid;
  grid-gap: 30px;
  grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
  grid-auto-rows: max-content;
  grid-auto-flow: row dense;
}

.article a,
.article a:visited {
  text-decoration: none;
  color: inherit;
}

.article img {
  width: 100%;
}

.error {
  display: flex;
  justify-content: center;
  align-items: center;
  height: 100%;
}

.alert {
  display: inline-block;
  padding: 15px;
  border-radius: 0.25rem;
}

.alert--error {
  background-color: #f8d7da;
  color: #721c24;
}

Under the home component. we'll add the functionality to fetch for the news using the newsapi.org API. If you haven't done so already, sign up for a free account to get your own API key. Open up the file at /src/routes/home/index.js and replace with the following.

import { h, Component } from "preact";
import style from "./style";

const apiKey = "YOUR_NEWS_API_KEY";

export default class Home extends Component {
  constructor(props) {
    super(props);
    this.state = {
      error: null,
      isLoaded: false,
      selectedSource: "techcrunch",
      articles: [],
      sources: []
    };
  }

  componentDidMount() {
    this.updateNews(this.state.selectedSource);
    this.updateSources();
  }

  updateNews = async (source = this.state.selectedSource) => {
    try {
      const res = await fetch(`https://newsapi.org/v2/top-headlines?sources=${source}&apiKey=${apiKey}`);
      const json = await res.json();

      this.setState(() => ({
        isLoaded: true,
        articles: json.articles,
        selectedSource: source
      }));
    } catch (error) {
      this.setState(() => ({ error }));
    }
  };

  updateSources = async () => {
    try {
      const res = await fetch(`https://newsapi.org/v2/sources?apiKey=${apiKey}`);
      const json = await res.json();

      this.setState(() => ({
        sources: json.sources
      }));
    } catch (error) {
      this.setState(() => ({ error }));
    }
  };

  render() {
    const { error, isLoaded, articles } = this.state;
    if (error) {
      return (
        <div className="error">
          <div className="alert alert--error">Error: {error.message}</div>
        </div>
      );
    } else if (!isLoaded) {
      return <div>Loading...</div>;
    } else {
      return (
        <div className={style.home}>
          <div className="site-header">
            <div className="select">
              <select
                value={this.state.selectedSource}
                onChange={e => {
                  this.updateNews(e.target.value);
                }}>
                {this.state.sources.map(source => {
                  return (
                    <option value={source.id} key={source.id}>
                      {source.name}
                    </option>
                  );
                })}
              </select>
            </div>
          </div>
          <main>
            {articles.map((article, index) => (
              <div className="article" key={index}>
                <h2>
                  <a href={article.url}>{article.title}</a>
                </h2>
                <img src={article.urlToImage} alt="" />
                <p>{article.description}</p>
              </div>
            ))}
          </main>
        </div>
      );
    }
  }
}

Your app should now be getting some news articles, with the default dropdown source being techcrunch. If you change the dropdown to a different source, it will pull in a different set of articles. Let's open up our service-worker.js file and take a peek.

workbox.precaching.precacheAndRoute(self.__precacheManifest || []);

The Service Worker uses Workbox to precache any files that will be generated by Preact.js during the build process. This is much like the default behavior provided by CRA mentioned above. However, we also want to detect and cache the news articles fetched by the News API. We'll simply replace the contents with the following:

// Default Precache for files generated by Preact.js
workbox.precaching.precacheAndRoute(self.__precacheManifest || []);

// Detect and register any fetch calls using 'https://' and use the Network First Strategy by Workbox
workbox.routing.registerRoute(/(?:https:\/\/.*)/,workbox.strategies.networkFirst());

// Handle any images
workbox.routing.registerRoute(
  // Cache image files
  /.*\.(?:png|jpg|jpeg|svg|gif)/,
  // Use the cache if it's available
  workbox.strategies.cacheFirst({
    // Use a custom cache name
    cacheName: "image-cache",
    plugins: [
      new workbox.expiration.Plugin({
        // Cache only 20 images
        maxEntries: 20,
        // Cache for a maximum of a week
        maxAgeSeconds: 7 * 24 * 60 * 60
      })
    ]
  })
);

Using Workbox, we can quickly and easily detect fetch requests and deal with them using Workbox Strategies. There are also a variety of Workbox Recipes to help with things like image caching and Google Fonts. With this added, our Progressive Web App is done! It's still very basic, but it will load offline and cache the news articles properly. We can do the final build and preview our app.

# Build and serve the assets
npm run serve

When you serve with Preact, it will prompt you to accept some permissions so that it can load over https. If you navigate to https://localhost:8080, open up your Chrome inspector and head over to the Application tab. Make sure the service-worker.js is active and Workbox is working. You might need to reload the page a couple of times for the caching to kick in.

Preact Service Worker

If things don't look right, try clearing the Application's cache under Clear storage > Clear site data and reload the page. You can simulate going offline by checking the Offline box under Service Workers. The app should still load the Tech Crunch articles even if offline. If you browse to other sources before going offline, those should also be cached and served. Sources that you didn't visit will result in an error if you try selecting them while offline.

To audit your PWA, use Google's Lighthouse in the Chrome Dev Tools. It will simulate a mobile device and throttle the internet to 3G speeds, eventually giving you some scores and advice for improvement.

PWA Lighthouse Audit

You can also host the app a variety of ways since it's essentially a static website after the build process. Visit your hosted app or the example app using an Android device with Chrome, and you'll see the prompt to add the app to your Home Screen. Apple iOS 11.3 and above will also support Progressive Web Apps, but I'm not sure Safari on mobile will pop up a prompt like Chrome does.

Hopefully this was a good intro into Progressive Web Apps. You can take things further by experimenting with more of Google Workbox's recipes and features, making your PWA even better!

Preact.js News Feed PWA ( view source )