New version notification for a React PWA made with Create React App

Gareth Cronin
4 min readFeb 24, 2023

--

I build side projects and my front-end of choice is a mobile-first, Progressive Web App (PWA), built in React and Material UI.

I find PWAs work really well. I’ve even been able to install and run them on old low-spec tablets with unsupported Android versions. Modern (and even not-so-modern) browsers recognise when they are visiting a PWA that meets the installable criteria and offer to “install” it. Installing it on a mobile device means an “app” is added to the device with a name and icon from the PWA’s manifest. When the user runs the app, it opens a full screen browser frame that is independent of the regular mobile browser. This gives the user a mobile experience, without me having to deliver a native mobile app.

PWAs use a service worker to manage the state of the app and orchestrate calls. One of the features of the system is to protect the installed version. This can be confusing when first developing a PWA, because even once the code is deployed to the web server and the user visits it, their local installation will continue to use the previous version of the code until they exit the app and restart it. This article explains why in detail and offers a number of ways to manage the user experience.

The approach I settled on is to raise a “toast” — or “snackbar” in Material UI parlance that tells the user that a new version is available and let’s them know they’ll need to exit the app to receive it. Here it is in action:

One of my apps “Bar Tool” showing the new version notification at the bottom

Create React App bootstrapping

I start from the Progressive Web App (PWA) template that Create React App provides, instantiating the template into a new subdirectory called web:

npx create-react-app web --template cra-template-pwa

That leaves a nice clean React app all ready to go as a PWA with a basic service worker — although the worker is disabled by default.

In the src/index.js file is a line that looks like this:

// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://cra.link/PWA
serviceWorkerRegistration.unregister();

To get the service worker running, all that is necessary is to follow that instruction and make the unregister() a register(). What the comment doesn’t say is that the register method has a handy callback that we can use to tell the user when a new version is available.

Listening for a new version

The register method can take an object as an argument with callback functions, one of which is called when a new version is available. To interact with the user though, I needed to somehow get a reference to that callback into the front end JSX. I found this handy guide to doing this in a Redux-based application, but Redux and friends are overkill for my tiny apps: I just use React hooks to manage state.

My solution is to create an empty object and pass it as a property to the root App component. This is what I put in index.js:

const callback={};

ReactDOM.render(
<ThemeProvider theme={theme}>
<CssBaseline />
<App callback={callback}/> {/* passing the callback */}
</ThemeProvider>,
document.querySelector('#root'),
);

serviceWorkerRegistration.register({
onUpdate: () => {
if(callback.onUpdate){
callback.onUpdate(); // delegating the callback
}
}
});

The code in the onUpdate method checks to see if I have registered a listener, and passes the call to it when the service worker’s listener fires.

In the root App.js React component, I use a useEffect hook to register the call back and a useState hook to raise the toast:

import propTypes from 'prop-types';
import * as React from 'react';
import { Box, Container, Snackbar } from '@mui/material';

//...

export default function App({callback}) {

const [snackbarOpen, setSnackbarOpen] = React.useState(false);
const [snackbarMessage, setSnackbarMessage] = React.useState('');


React.useEffect(() => {
callback.onUpdate = () => {
console.log('service worker update waiting');
setSnackbarMessage('A new version is available: exit the app to update');
setSnackbarOpen(true);
};
}, []);


return (
<Container maxWidth="md"
sx={{ borderStyle: "solid", borderWidth: 1, borderRadius: '3px' }}>
<Box sx={{ my: 4 }} alignItems="center">

{//...}

</Box>

<Snackbar
open={snackbarOpen}
autoHideDuration={60000}
message={snackbarMessage}
onClose={() => setSnackbarOpen(false)}
/>
</Container>

);
}

App.propTypes = {
callback: propTypes.object
};

Summary

It’s a simple solution and it works. Ideally I’d also provide an action on the toast so the user could reload the app with a button push — but that’s for another day.

--

--

Gareth Cronin

Technology leader in Auckland, New Zealand: start-up founder, father of two, maker of t-shirts and small software products