Adding update button to Gatsby app

03/28/20202 Min Read — In React, Gatsby, PWA

When using Gatsby.js to create an app, you probably use gatsby-plugin-offline and service worker to get features like offline mode or "Add to home screen" notification. It's great but you may encounter issues with keeping users app up to date.

Why show Update App button?

Gatsby will trigger reload when changing the route. If a user is doing something in the app, he will likely have to start again. That's not good user experience. And here's why update button is really useful.

We can wait with app update (new service worker activation and app refresh) until the user is ready until they click "Update App" button!

Go to the end of the article for quick solution (service-worker-updater package).

New service worker = new app update

Why check for SW updates? When Gatsby app is deployed, there are generated new files (with unique filenames) and new service worker has a list of them. So always when the app is updated, it will change. That's why we can tell that there's a new update when the service worker has changed.

When a service worker is updating?

SW doesn't get real-time updates and if users rarely refresh the app (because why would one do it often), they will end up using the old version.

Service worker checks for updates on :

  • A navigation to an in-scope page.
  • A functional events such as push and sync, unless there's been an update check within the previous 24 hours.
  • Calling .register() only if the service worker URL has changed. However, you should avoid changing the worker URL.

(source)

How to trigger an update?

To trigger an update, you can periodically run the following code.

navigator.serviceWorker.register("/sw.js").then(reg => {
reg.update()
})

It checks for a new service worker. When one is found, it's installed and will be ready to use after the page refresh.

How to prevent auto reload on page navigation?

Pretty easy. Add this to gatsby-browser.js file:

export const onServiceWorkerUpdateReady = () => {
// Set window.___swUpdated to prevent update on page navigation.
// Overrides https://github.com/gatsbyjs/gatsby/tree/master/packages/gatsby/cache-dir/navigation.js#L64
window.___swUpdated = false
}

Now, the user will have a service worker installed, but it will wait for reloading to fetch new app files and start working. (Gatsby API)

Monitoring SW changes and showing the update button

Register service worker.

const reg = await navigator.serviceWorker.register("/sw.js")

Check the current state. If it's waiting - there's a new update.

if (reg.waiting) {
showUpdateButton(reg.waiting)
}

Otherwise, wait for updatefound event.

reg.addEventListener("updatefound", () => {
if (reg.installing) {
trackInstalling(reg.installing)
}
})
function trackInstalling(worker) {
// if NEW service worker state changes to 'installed' or 'waiting',
// there's an update ready
worker.addEventListener("statechange", () => {
if (["installed", "waiting"].includes(worker.state)) {
showUpdateButton(worker)
}
})
}

How to actually update? I know there's an update ready.

Post to your SW message { type: "SKIP_WAITING" } and reload the app.

function showUpdateButton(worker) {
worker.postMessage({ type: "SKIP_WAITING" })
window.location.reload(true)
}

Taking full control over SW installation

If you'd like to wait for new SW activation, you can edit the gatsby-plugin-offline config and set skipWaiting setting to false. This will not activate new SW until you do it in your code (or reload browser). Read more about skipWaiting here.

{
resolve: `gatsby-plugin-offline`,
options: {
workboxConfig: {
// this prevents activating new service worker
skipWaiting: false,
},
},
}

But... Maybe leave default option (true), because if you'll have some serious rendering error in one of updates and user will keep getting white screen = "Update app" button will never appear. 😯

🎁 Check out service-worker-updater

I created service-worker-updater package which will check for app/SW updates for you. There's useSWUpdateChecker React Hook and withSWUpdateChecker HOC (Higher Order Component), use whichever works for you the best.

Just create a component like <UpdateButton />. Below code will check for app updates every 10 minutes and only show button if there's an update available.

import React from "react"
import { useSWUpdateChecker } from "service-worker-updater"
const CHECK_INTERVAL = 10 * 60 * 1000 // 10 minutes
const UPDATE_ON_LOAD = false
export default function UpdateButton() {
const [hasUpdate, updateHandler] = useSWUpdateChecker({
checkInterval: CHECK_INTERVAL,
updateOnLoad: UPDATE_ON_LOAD
})
if (!hasUpdate) return <div />
return (
<button
onClick={() => {
updateHandler()
}}
>
Update app
</button>
)
}

This package can be used with any React app, not only Gatsby (but remember to handle properly incoming post messages in service worker).

Demo Gatsby app with Update button

Here you go, sample implementation with Update App button: https://github.com/cactoo/gatsby-update-button 👍

Summary

I hope this article was helpful for you. If you have any questions or suggestions, feel free to contact me. 👋