Single Page Applications (SPAs) have a significant advantage over traditional multi-page applications in that they load assets when a client initially accesses the app. This means that routing can be very fast; the user doesn’t have to load a new HTML file every time they navigate within the application. But this advantage also creates a unique problem: after deployment, users will only have the latest version of your app once they refresh the site.

Create React App uses Webpack to generate new builds, and Webpack will automatically hash filenames to make sure the client doesn’t hold on to old assets. Assuming your cache is properly configured, all you need from the user is a refresh to ensure they get the changes from your last deployment.

Alerting the User

So how do we let the user know they should refresh the app? Well, you may have seen an increasingly popular pattern for handling this: the new version available alert.

New version notification in Inbox (Screenshot)

Google Inbox (RIP) letting you know their team has deployed a new version

This pattern is used in many of Google’s products, including the Firebase Dev Console and Android Messages for web. It’s also used by my favorite budgeting app, YNAB. There are several ways to detect a version change on the frontend, including using service workers, regular calls to a server, and timers that run a check on index.html. All of these work, but they’re also fairly involved.

Because we use Firestore as our database on Datapage, we’re in a uniquely advantageous position when it comes to notifying our users about something. Each user already has an active connection to Firestore, and they’re already subscribing to changes in the database to display updates in real-time. If we can track the version of our app inside a Firestore document, the client can listen for a change in that document’s fields, and display a notification when the fields change in value.

Version numbers in Cloud Firestore (screenshot)

Now we’ll need a component that listens for the change, and displays an alert. We use the Material-UI for building frontend components and react-redux-firebase for connecting to Firestore, but native Firestore web functions will work just as well here.

First, let’s create a new component that listens for a change in the version document of our config collection:

We initialize a state variable called versionChanged. We then connect to Firestore and subscribe to our version document and return the number field. When version changes, two side effects run. First, we set a ref with the value of the previous version so we can compare it to the new one. Then we compare the two versions and if they’re different, we update versionChanged to true.

Now that we know when the version changes, all that’s left is the UI.

New version indicator in SPAs (screenshot)

Our Material-UI snackbar will open when versionChanged is set to true. Our refresh button calls on a function that is dead simple:

Feel free to stop here. But let’s say we wanted to get more aggressive than Google does in making sure every user is on the updated version of the app? We could display a countdown timer and force a refresh after a given interval.

Counting Down to Refresh

First, let’s add another state variable and two more side effects.

Our first effect will be triggered as soon as versionChanged is true and as long as secondsSinceChange is less than 30. We initialize secondsSinceChange as 0, and begin counting upwards, adding 1 to the value every second after our first side effect fires. As soon as secondsSinceChange reaches 29, we force a refresh of the app.

This is a big improvement, but we should probably let the user know what’s going on. Let’s update our alert to display a progress spinner which will fill up as the timer counts to 30, with the number of seconds remaining in the middle.

New version warning in SPA (screenshot)

We create a new Circular Progress MUI component and set its value to secondsSinceChange * 3.33, converting our seconds counted to a value n/100, filling the circle up according to the Circular Progress component API. Inside the Circular Progress, we have an absolutely positioned numeric representation of how many seconds the user has before the app will refresh for them.

Automating Version Changes

This will work just fine, as long as you don’t forget to update your project’s version number by hand in Firestore each time you deploy a new version of the app. Let’s make it easier to not forget by automating the process.

Deployment processes vary widely, so I’ll just cover the process we use. The first step is to create an onRequest Cloud Function which will update our version number.

Now, all we need to do is to make a request with a parameter of the new version number as the body. In Datapage, we handle all of our deployments through NPM scripts from our dev machine. Here’s what our deployment script looks like:
This single command calls each step of our deployment process, from building the project, to backing up the old deployment, to copying the build files to our server. Let’s add one more script which will be executed last, only after the other steps have been completed:
We’re calling a shell script and passing it two arguments. The first argument is what you could call a “hidden” feature of NPM. Every entry in your package.json is accessible when running a script with npm run, prefaced with npm_package. Because we always update version in our package.json when we deploy, we don’t have to change our deployment process to make sure we update Firestore with our new version number. The second argument is the Firebase project id that the Cloud Function should be run from.

Let’s take a look at that shell script:

We’re making use of an overlooked feature of the Google Cloud CLI – it can call Cloud Functions directly. First, we set gcloud’s active project to the id we sent as our second argument. Then, we call the function and send back an object with a single field, version, with the version number of our package.json.

Let’s recap how this all works:

  1. We commit our new version of the app, updating the version in package.json.
  2. We run npm run deploy
  3. React builds the app for production, and transfers it to the server
  4. Node calls our script, which calls our Cloud Function, which updates Firestore with the new app version number
  5. Our component detects the change and fires a series of effects that display the alert: starting a countdown to an automatic browser refresh

The end result is that we won’t have to change anything about when and how we deploy, but we’ll be able to make some pretty safe assumptions about our users being up to date.

Tagged with →  

Leave a Reply

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

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Share →
DayBack Calendar
DayBack's 30-day trial is unlocked so you can customize it and integrate it with your files.
Download DayBack and we'll send you a couple short emails with tips on how to modify it and use some of the coolest features.
Thank you! Please download: DayBack Calendar
Need More?
SeedCode tips & example files in your inbox
Need More?
SeedCode tips & example files in your inbox
Want More?
Be the first to see articles and tips like these
Download TimeZync and we'll send you a couple short emails with tips syncing your FileMaker Go files.
Thank you! Please download: TimeZync
Want More?
Be the first to see articles and tips like these