DEV Community

Jono Cairns
Jono Cairns

Posted on

Create react app version check

The create react app is a great tool to bootstrap any new project you're working on. They bundle a bunch of useful tool chains in to one single package so you can hit the ground running. Here are some of the things it provides out of the box.

  • React, JSX, ES6, TypeScript and Flow syntax support.
  • Language extras beyond ES6 like the object spread operator.
  • Autoprefixed CSS, so you don’t need -webkit- or other prefixes.
  • A fast interactive unit test runner with built-in support for coverage reporting.
  • A live development server that warns about common mistakes.
  • A build script to bundle JS, CSS, and images for production, with hashes and sourcemaps.
  • An offline-first service worker and a web app manifest, meeting all the Progressive Web App criteria. (Note: Using the service worker is opt-in as of react-scripts@2.0.0 and higher)
  • Hassle-free updates for the above tools with a single dependency.

With this you can add something like react-router and you have the bones for a new SPA (single page application).

That's all great but since it's a single page application, how do people using the site know that there's a newer version available? This is especially important if you have updated API contracts in a deployment.

Unless you have a defined pipeline to do this already I've got a cheap and easy way to inform your users that they may need to refresh the page to get the latest changes.

The create-react-app creates a manifest.json file when the yarn build command is run. This file essentially tells the application where/what files exist. The file names are hashed for each build. This means we can tell if something has changed, as long as we poll this manifest file somehow...

So we need to create a component of sorts that can sit at a high level, it needs to be responsible for polling this manifest and telling the UI if there has been a change.

Here's an example I wrote, using material UI to display a snackbar whenever the version had changed.

import {Button} from '@material-ui/core';
import {CloseOutlined} from '@material-ui/icons';
import {useSnackbar} from 'notistack';
import React, {useState} from 'react';

const MANIFEST = '/asset-manifest.json';
const POLL_INTERVAL = 60000;

export const VersionCheck: React.FC = ({children}) => {
  const {enqueueSnackbar, closeSnackbar} = useSnackbar();
  const [dismissedVersion, setDismissedVersion] = useState('');

  React.useEffect(() => {
    const getLatestVersion = async () => {
      const response = await fetch(MANIFEST);
      return await response.text();
    };

    const init = async () => {
      try {
        const latestVersion = await getLatestVersion();
        localStorage.setItem('tend-version', latestVersion);
      } catch (ex) {
        // log to sentry / or something
      } finally {
        setTimeout(poll, POLL_INTERVAL);
      }
    };

    const poll = async () => {
      try {
        const currentVersion = localStorage.getItem('tend-version');
        const latestVersion = await getLatestVersion();

        if (currentVersion && currentVersion !== latestVersion && latestVersion !== dismissedVersion) {
          enqueueSnackbar('A new version is available', {
            variant: 'info',
            persist: true,
            preventDuplicate: true,
            action: (key) => (
              <>
                <Button color="inherit" onClick={() => window.location.reload()}>
                  Refresh
                </Button>
                <Button
                  color="inherit"
                  variant="text"
                  onClick={() => {
                    setDismissedVersion(latestVersion);
                    closeSnackbar(key);
                  }}>
                  <CloseOutlined />
                </Button>
              </>
            ),
          });
        }
      } catch (ex) {
        // log somewhere
      } finally {
        setTimeout(poll, POLL_INTERVAL);
      }
    };

    if (process.env.NODE_ENV === 'production') {
      init();
    }
  }, [closeSnackbar, dismissedVersion, enqueueSnackbar]);

  return <>{children}</>;
};
Enter fullscreen mode Exit fullscreen mode

This would display the following snackbar when the deployed files had changed.

alt text.

Why would you want this? With a SPA there's sometimes no need at all to refresh the website to get the latest files. This means clients consuming your APIs can potentially have very out of date code running. If you have made key contract changes between your API and clients you'll need to tell them somehow. This can potentially short circuit bug reports about recently touched parts of the UI not working correctly.

Latest comments (6)

Collapse
 
khorengrig profile image
khorengrig

Great Article Thanks. But my opinion service-worker for this case is a better solution. You can easily detect if site new version available, and notify your users. And its also enables good content caching.

Collapse
 
jonocairns profile image
Jono Cairns

Yep - there definitely are more intricate solutions, the cost is more time to build. deanhume.com/displaying-a-new-vers... is a pretty good guide on how to use service workers for the same thing. My example is mainly for when you need something quick and simple without having to deal with the potential complexity that service workers can bring.

Collapse
 
khorengrig profile image
khorengrig

Agree with you. Service worker can bring complexity. But
Google Workbox makes it easier. A few months ago I had a problem with chunk errors. My users were getting this erros (during navigation, because the requested file had already been deleted from server) every time when I was deploying new a content in live. I tried multiple ways to solve this problem(including poling to server with timeout). And also had problem with content caching, and my solution was SW.

Thread Thread
 
jonocairns profile image
Jono Cairns • Edited

Nice - thanks for posting that :) Caching files with a SPA is always tough, there are so many layers things can go wrong and debugging is non-trivial. How did you end up informing the user that you essentially needed to block the UI and prompt a reload? Or did you just onError refresh the page so it'd grab the latest?

Thread Thread
 
khorengrig profile image
khorengrig

I am just roughly refreshing all opened tabs (if they are multiple) and they grab latest content, without any side action and SW automatically revalidates it's cache. But would be nice to inform user to do it yourself :D (with a custom prompt).

Thread Thread
 
jonocairns profile image
Jono Cairns

Ah I see. Our application has a voice/video call function so we're unable to refresh randomly, as someone could be in a call :(