DEV Community

Cover image for Handling Service Worker updates in your Vue PWA
Drew Bragg
Drew Bragg

Posted on • Updated on

Handling Service Worker updates in your Vue PWA

Table Of Contents

Vue.js is awesome. It's easy to use, extremely flexible, and has some awesome DevTools. Since you're reading this I'll assume you already know this.

The Vue CLI is one such DevTool, allowing us to quickly and easily add plugins to our Vue App. Given the growing trend and popularity of building PWA's it comes as no surprise that the Vue CLI has its own PWA plugin and, for the most part, it's as awesome as you'd expect.

If all you're trying to do is add some basic PWA magic to your site the plugin-pwa is pure magic. Just install it and out of the box you get your manifest for install-ability and a service worker for precaching. There's even a host of configuration options if you want to get fancy with a theme color, change your PWAs name, etc.

What it doesn't do out of the box is handle activating the service worker when an updated one is found. So let's add that ourselves.

Updating the Service Worker registration

When you install the plugin-pwa it adds a registerServiceWorker.js file to src with some basic config and events. For more on this file feel free to checkout register-service-worker on npm. The only part we need (for this tutorial) is the update() function. On a fresh install it looks like this:

updated () {
  console.log('New content is available; please refresh.')
}
Enter fullscreen mode Exit fullscreen mode

We'll need to modify this function a bit to get it to do more than just log to our console when there's an update.

First things first, we'll need access to the new service worker that was just registered. Luckily register-service-worker handles this for us. According to their documentation:

The ready, registered, cached, updatefound and updated events passes a ServiceWorkerRegistration instance in their arguments.

Perfect! Simply pass the ServiceWorkerRegistration in as an argument and we're off to the races. The next issue we'll face is getting those registration details to our Vue app. So, we can use a CustomEvent to handle that. Now our update() function should look something like this:

updated(registration) {
  console.log('New content is available; please refresh.')
  document.dispatchEvent(
    new CustomEvent('swUpdated', { detail: registration })
  )
}
Enter fullscreen mode Exit fullscreen mode

We're now passing in our ServiceWorkerRegistration and triggering an event we can listen to called swUpdated and sending the ServiceWorkerRegistration as an event property.

Making an update mixin

Next up is listening for this event from within our Vue app. There are many places you can put this code, depending on your projects structure, but I opted to make it a mixin. Just personal preference, you do you. Let's create a file in src called mixins/update.js and set it up to listen for our event and make a callback when it's triggered:

export default {
  created() {
    document.addEventListener('swUpdated', this.updateAvailable, { once: true })
  },
  methods: {
    updateAvailable(event) {
      console.log(event)
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

A note about the once option; setting this option to true allows the listener to be called only once AND removes the listener once invoked.

Let's store the SW registration so we can use it later in the update process. While we're at it we can add a flag to control showing our future 'Update available; please refresh.' message to our user. Should look something like this:

export default {
  data() {
    return {
      registration: null,
      updateExists: false,
    }
  },
  created() {
    document.addEventListener('swUpdated', this.updateAvailable, { once: true })
  },
  methods: {
    updateAvailable(event) {
      this.registration = event.detail
      this.updateExists = true
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Updating our UI

One of the reasons why I used a mixin for this is so I can easily use this functionality anywhere I want in my app (App.vue, a layout, somewhere else) and with any UI kit I'm using on that project. I love Vuetify so for the sake of this tutorial lets roll our 'Update' message to our user with that.

And for simplicity lets just throw in it our App.vue file. Again, you can do this wherever is right for your app.

In your App.vue template add a snackbar component with a button that will allow the user to update the app when prompted. Something like this:

<v-snackbar bottom right :value="updateExists" :timeout="0" color="primary">
  An update is available
  <v-btn text @click="refreshApp">
    Update
  </v-btn>
</v-snackbar>
Enter fullscreen mode Exit fullscreen mode

You'll also need to import the update mixin. Because we're adding the mixin we'll have access to all the data and functions of the mixin.

Skipping Service Working waiting

Let's pop back into our update mixin and create the refreshApp function. We'll use this function to reset the updateExists flag and force the new service worker to become the active one. Once a service worker is registered it "waits" until the perviously registered SW is no longer controlling the client. By telling the new SW to "skip waiting" we literally skip this waiting period.

Our refreshApp function will look a little something like this:

refreshApp() {
  this.updateExists = false
  // Make sure we only send a 'skip waiting' message if the SW is waiting
  if (!this.registration || !this.registration.waiting) return
  // Send message to SW to skip the waiting and activate the new SW
  this.registration.waiting.postMessage({ type: 'SKIP_WAITING' })
}
Enter fullscreen mode Exit fullscreen mode

Updating our Service Worker

If you're using the default settings for plugin-pwa or you have workboxPluginMode set to 'GenerateSW' you can skip this next part as the plugin automatically generates a service worker with the proper listener. Otherwise you need to add the following listener to your service worker after your standard workbox config:

self.addEventListener('message', (event) => {
  if (event.data && event.data.type === 'SKIP_WAITING') {
    self.skipWaiting()
  }
})
Enter fullscreen mode Exit fullscreen mode

We're almost done. Now we just need to reload the page once the new service worker is active so our changes can be seen.

Reloading the page

Back in our update mixin lets listen for the controllerchange event from our service worker.

In created() add:

navigator.serviceWorker.addEventListener('controllerchange', () => {
  // We'll also need to add 'refreshing' to our data originally set to false.
  if (this.refreshing) return
  this.refreshing = true
  // Here the actual reload of the page occurs
  window.location.reload()
})
Enter fullscreen mode Exit fullscreen mode

And that's it! Deploy this update and manually clear your apps storage. Then deploy another update, refresh the page, and you should see your popup:

Clicking the update button should trigger the site to reload and you'll see your changes!

TL;DR

  • Update serviceworker registration:
// src/registerServiceWorker.js

// Standard SW registration script.
// Auto generated by the Vue CLI PWA Plugin
import { register } from 'register-service-worker'

if (process.env.NODE_ENV === 'production') {
  register(`${process.env.BASE_URL}service-worker.js`, {
    //...
    // When the SW is updated we will dispatch an event we can listen to in our .vue file
    updated(registration) {
      console.log('New content is available; please refresh.')
      document.dispatchEvent(
        new CustomEvent('swUpdated', { detail: registration })
      )
    },
    //...
  })
}
Enter fullscreen mode Exit fullscreen mode
  • Make an update mixin:
// src/mixins/update.js

export default {
  data() {
    return {
      // refresh variables
      refreshing: false,
      registration: null,
      updateExists: false,
    }
  },

  created() {
    // Listen for our custom event from the SW registration
    document.addEventListener('swUpdated', this.updateAvailable, { once: true })

    // Prevent multiple refreshes
    navigator.serviceWorker.addEventListener('controllerchange', () => {
      if (this.refreshing) return
      this.refreshing = true
      // Here the actual reload of the page occurs
      window.location.reload()
    })
  },

  methods: {
    // Store the SW registration so we can send it a message
    // We use `updateExists` to control whatever alert, toast, dialog, etc we want to use
    // To alert the user there is an update they need to refresh for
    updateAvailable(event) {
      this.registration = event.detail
      this.updateExists = true
    },

    // Called when the user accepts the update
    refreshApp() {
      this.updateExists = false
      // Make sure we only send a 'skip waiting' message if the SW is waiting
      if (!this.registration || !this.registration.waiting) return
      // send message to SW to skip the waiting and activate the new SW
      this.registration.waiting.postMessage({ type: 'SKIP_WAITING' })
    },
  },
}
Enter fullscreen mode Exit fullscreen mode
  • Update the UI (vuetify example):
// src/App.vue

// I use Vuetify in almost all of my Vue apps so this is how __I__ handle alerting the user to an update.
// Your implementation may change based on your UI
<template>
  <!-- normal vue views stuff here -->
  <!-- ... -->
  <v-snackbar bottom right :value="updateExists" :timeout="0" color="primary">
    An update is available
    <v-btn text @click="refreshApp">
      Update
    </v-btn>
  </v-snackbar>
</template>

<script>
import update from './mixins/update'

export default {
  name: 'App',
  data: () => ({
    //
  }),
  mixins: [update],
  ...
}
</script>
Enter fullscreen mode Exit fullscreen mode
  • Update the Service Worker:
// src/service-worker.js

// If you are using 'GenerateSW' (default) for your workboxPluginMode setting this file is auto generated for you.
// If you are using 'InjectManifest' then add this to your custom SW after your standard workbox config

self.addEventListener('message', (event) => {
  if (event.data && event.data.type === 'SKIP_WAITING') {
    self.skipWaiting()
  }
})
Enter fullscreen mode Exit fullscreen mode

Boom, Done.

So, what do you think? Anything about my implementation you would change? Do you handle SW updates differently? Hell, tell me if you just don't like my writing style. I won't get better or have the confidence to write more posts without your feedback!

Top comments (34)

Collapse
 
drbragg profile image
Drew Bragg • Edited

I just found out that this article is being included in the Vue.js Developer Newsletter which is pretty exciting!

If you found this article via the newsletter let me know.

Collapse
 
pimhooghiemstra profile image
Pim Hooghiemstra

I am not totally sure, but I think it would be fair to add a reference to this article on medium medium.com/@dougallrich/give-users... where the author explains this whole concept. I coded my version of a PWA with updated SW last year based on this article and I found that your code samples are more or less identical to what is described in this article.

Collapse
 
drbragg profile image
Drew Bragg

Wow, that is super similar in code examples. I read a bunch of different articles and docs while I was working on my solution. Somehow, I didn't see this one.

Thanks for this link and the idea. Maybe I can add a list of different articles that take a similar approach to this problem.

Collapse
 
frynt profile image
frynt • Edited

YOU saved my life.

To automatically show the update banner, add this in service worker :

dev-to-uploads.s3.amazonaws.com/up...

Collapse
 
fotiecodes profile image
fotiecodes • Edited

Hi man, thanks again for this amazing article. i just stumbled into an issue, i'm not sure how to explain this though. but when i deploy a version of my code to production it still uses the reference to the old fines generated by the build. for example. The newly generate build files in my index.html are /js/chunk-vendors.3c199704.js however my webapp tries to reference an old generate file which isn't existing.

Note that when i clear the catch everything starts working properly. I don't really know what is going on at this point. Please anyone knows how i can fix this?

Collapse
 
fotiecodes profile image
fotiecodes

Anyone has any idea on how to fix this issue? again it was working pretty well but started bugging in production lately!

Collapse
 
fotiecodes profile image
fotiecodes • Edited

Thanks for this awesome article man... really helped!

Collapse
 
fsboehme profile image
fsboehme

Thank you, Lord! You just saved my sanity – or what's left of it after struggling with this for 2 days! Amazing! Thank you so much for this! 🙌 🙌 🙌

tiny fix: there seems to be a comma missing between updateAvailable and refreshApp in methods.

Collapse
 
drbragg profile image
Drew Bragg

Great! The whole reason I wrote this up was to help someone dealing with the same issue. I'm super happy I could help.

Thanks for the heads up on the comma 🤦‍♂️

Collapse
 
milindsingh profile image
Milind Singh

In my blog this updated(registration) method is called on every page reload and it show in console that 'New content is available; please refresh.'

Is this usual for PWA or I missed something ?

updated(registration) {
  console.log('New content is available; please refresh.')
  document.dispatchEvent(
    new CustomEvent('swUpdated', { detail: registration })
  )
}
Enter fullscreen mode Exit fullscreen mode

dev.adapttive.com

Collapse
 
drbragg profile image
Drew Bragg

Hey Milind, that doesn't sound right to me. Did you follow the above example exactly?

Collapse
 
milindsingh profile image
Milind Singh

Yes, I found the issue.

I had add the onesignal sdk for notifications, which has a service worker causing conflit with default website service worker. 🙂

Collapse
 
dharmendradavid profile image
dharmendradavid

Thank you for this wonderful article. This is working fine if we refresh the page and showing an update prompt. But this not working without a refreshing page. Especially in the case of SPA. Please help me to show update prompt without refresh page

Collapse
 
ajlozier profile image
Aaron Lozier

I had the same question. Turns out you can create an interval to poll for updates in registerServiceWorker.js:

 registered(registration) {
      console.log(
        'Service worker has been registered and now polling for updates.'
      )
      setInterval(() => {
        registration.update()
      }, 5000) // every 5 seconds
    }
Enter fullscreen mode Exit fullscreen mode
Collapse
 
frynt profile image
frynt

This works perfectly ! thanks !

Collapse
 
positivethinking639 profile image
positivethinking639

Hi. I want to ask. I had try your code and it works. I don't want to show the snackbar message. So I deleted this code : <v-snackbar bottom right :value="updateExists" :timeout="0" color="primary">
An update is available
<v-btn text @click="refreshApp">
Update
</v-btn>
</v-snackbar>
. It worked and there were no problems. Changes are automatically updated without clicking the update button. How do you think?

Collapse
 
positivethinking639 profile image
positivethinking639 • Edited

I try another option like this : methods: {
updateAvailable (event) {
this.registration = event.detail
this.updateExists = true
this.refreshApp()
}
. So if detect new version, it immediately called refreshApp method. So user don't need to click the update button

Collapse
 
rdj profile image
RDJ

Snackbar with UPDATE button never goes away on Chrome/Firefox even after reloading, works as expected on Safari. Any help is appreciated @drbragg

Collapse
 
drbragg profile image
Drew Bragg

Could be a change in a newer of Vue or Vuetify. Or heck, could be a change in how register-service-wroker works (judging by some of the other comments). It's been a while since I worked with this. If it helps these were my dependency versions at the time:

"dependencies": {
  "register-service-worker": "^1.7.1",
  "vue": "^2.6.11",
  "vuetify": "^2.2.31"
}
Enter fullscreen mode Exit fullscreen mode
Collapse
 
rdj profile image
RDJ

@drbragg figured out, it was problem with using firebase serviceWorker being loaded at the same time. Thanks for this article, highly appreciate that

Thread Thread
 
drbragg profile image
Drew Bragg

Awesome! Glad you were able to figure it out!

Collapse
 
rdj profile image
RDJ

@drbragg thanks for responding. I’ll check it out

Thread Thread
 
emmanueled profile image
Emmanuele D

Hi @rdj

I'm trying to use this system and at the same time i have FCM on my project.
How did you solve that situation?

thankyou