Skip to content

Blogging On Rails

Everything on Rails!

Display your Progressive Web App’s Offline Ability

Your progressive web app works offline, but do your users know that? You can use a Stimulus controller to manage your service worker, show the caching status, and hint to your user that the webpage will work offline.

This tutorial will enhance the Hacker News Progressive Web App. It will show you how to cache the main pages, so that you can read the top stories offline, and it will display an icon to show the offline state.

The Service Worker

The Service Worker is now tasked to cache more material, such as the top pages, more images and the CSS and Javascript bundles. It now also waits to be claimed, which means that it expects a webpage will communicate directly with it. This is different from the passive version before, which only proxied requests and loaded an offline status page.

You can see the changes here: app/views/service_worker/service_worker.js.erb. The cached files are now placed in an array called REQUIRED_FILES, which can be updated as needed. The activation handler now calls event.waitUntil(self.clients.claim());, which loads this version of the service worker immediately.

Stimulus.js Service Worker Controller

Since the service worker now communicates and needs to direct changes on the page, it makes sense to move the registration code to something that we can better control. A stimulus controller that registers the service worker can then register for the callbacks, and update the page will do the trick.

The html will have two icons that the controller will toggle display the loading state and the completely cached state.

<div class="navbar-item" data-controller="service-worker">
  <%= image_tag "cloud-check.svg", 
      width: "23px", 
      alt: "Webpage saved for offline use", 
      data: { target:"service-worker.pageSavedNotice" }, 
      class: "is-hidden" %>
  <%= image_tag "cloud-download.svg", 
      width: "23px", 
      alt: "Saving webpage for offline use", 
      data: { target:"service-worker.savingPageNotice" } %>

In the connect() function, the controller sees if the Service worker is running. If it’s running, the controller shows the downloaded icon, alerting the user everything is ready for offline usage.

If the service worker isn’t running, the controller registers the service worker, and sets up a chain of callbacks, waiting for the installation and activation to complete. The downloaded icon is then swapped in for the downloading icon.

import { Controller } from "stimulus";

export default class extends Controller {
  static targets = ["pageSavedNotice", "savingPageNotice"];

  connect() {
    if (navigator.serviceWorker) {
      if (navigator.serviceWorker.controller) {
        // If the service worker is already running, skip to state change
      } else {
        // Register the service worker, and wait for it to become active
          .register("/service-worker.js", { scope: "./" })
          .then(function (reg) {
            console.log("[Companion]", "Service worker registered!");

  controllerChange(event) {

  stateChange() {
    let state = navigator.serviceWorker.controller.state;

    if (state === "activated" || state === "redundant") {

This is inspired by, with a Stimulus.js and Rails twist. Let me know how this works for you!

You can find all the code here:

Want To Learn More?

Try out some more of my Stimulus.js Tutorials.

Make Interactivity Default 

Make your web app interactive now with easy to implement and simple to add HOTWire integrations. 

Enter your email and get a free sample of my HOTWire Tutorials ebook.

We won’t send you spam. Unsubscribe at any time.

Leave a Reply

Your email address will not be published. Required fields are marked *