Cache Busting In React JS

Browser Caching:-  When the site is loaded on the web browser,  the browser caches some resources like- images, JS, CSS locally and when a user visits the site next time browser serves the file from the local cache. Since we do not have control on the user’s browser it is really hard to clear cache from the browser. If we are webpack or other header caching tools it becomes easier to manage, but still, these are not up to the mark.

Some points to understand with browser caching – 

  1. Browsers ignore cache validation if we refresh the site in the same tab, and also if the user opens a new tab there is a chance that the files loaded from the cache even if we clear the cache.
  2. If the app is registered with Service Worker, then the service worker invalidates the cache only if the user opens the site in a new tab. Users will face issues even after registering Service Worker if we never open the site in a new tab. 

Generally caching helps to load the site. Disable cache is not the right solution, it is also not good because we can not handle browser behaviour. We have to figure out a way to clear the browser or Service Worker cache every time if our app is deployed with a new version.

A simple effective way to handle – 

  • Bundle the app version into the app.
  • Generate a new  meta.json file with the newer version with each build.
  • Fetch meta.json file on-site loads from the server and compare the versions.
  • Clear all the saved cache and hard reload the site.

Bundle the app version into the app – 

Parse the version from the package.json file during creating build and set this as a global variable, so we can easily check the app version in the browser console as well as to compare with the latest version.

import packageJson from '{root_dir}/package.json';

global.appVersion = packageVersion.version;

By doing this we can check  the app version in the browser console by typing appVersion

Generate meta.json file on each build with unique app version-

We need to run a script to generate meta.json file before creating a build.

/* package.json */

{

    "scripts": {

        "generate-build-version": "node generate-build-version",

        "prebuild": "npm run generate-build-version",

        // other scripts

     }

}
/* generate-build-version.js */

const fs = require('fs');

const packageJson = require('./package.json');

const appVersion = packageJson.version;

const jsonData = {

  version: appVersion

};

var jsonContent = JSON.stringify(jsonData);

fs.writeFile('./public/meta.json', jsonContent, 'utf8', function(err) {

  if (err) {

    console.log('An error occured while writing JSON Object to meta.json');

    return console.log(err);

  }

  console.log('meta.json file has been saved with latest version number');

});

After deploying the app we can access the meta.json file with the path /meta.json and we can fetch the JSON as a REST endpoint. It won’t be cached by our browser because our browser does not cache XHR requests. So we will always get the latest app version even if our bundle is cached in the browser.

So if the appVersion in your build is less than from the appVersion in the meta.json file. we know that it’s time to clear the cached file from the browser. For comparing and clearing the cache to follow below code – 

const checkVersionMismatch = (letestVersion, currentVersion) => {

    const letestVersion= letestVersion.split(/\./g);  

    const currentVersion= currentVersion.split(/\./g);

    while (letestVersion.length || currentVersion.length) {

      const a = Number(letestVersion.shift());  

      const b = Number(currentVersion.shift());

      // eslint-disable-next-line no-continue

      if (a === b) continue;

      // eslint-disable-next-line no-restricted-globals

      return a > b || isNaN(b);

    }

    return false;

  };

Fetch meta.json on-site load and compare versions – 

When our app is loaded we need to fetch meta.json and compare the current version with the latest version available on the server.

If there’s a version mismatch clear all the cache and hard reload the site. If no version mismatch found no action required.

Code to create a component to handle this part (clearing cache and reload site ) is below.

import packageJson from '../package.json';

global.appVersion = packageJson.version;

const checkVersionMismatch = (letestVersion, currentVersion) => {

    const letestVersion= letestVersion.split(/\./g);  

    const currentVersion= currentVersion.split(/\./g);

    while (letestVersion.length || currentVersion.length) {

      const a = Number(letestVersion.shift());  

      const b = Number(currentVersion.shift());

      // eslint-disable-next-line no-continue

      if (a === b) continue;

      // eslint-disable-next-line no-restricted-globals

      return a > b || isNaN(b);

    }

    return false;

  };

  class HandleCache extends React.Component {

    constructor(props) {

      super(props);

      this.state = {

        isLoading: true,

        isLatestVersionAvailable: false,

        clearCacheAndReload: () => {

          console.log('Clearing cache and hard reloading...')

          if (caches) {

            // deleting saved cache one by one

            caches.keys().then(function(names) {

              for (let name of names) caches.delete(name);

            });

          }

          // after deleting cached data hard reload the site

          window.location.reload(true);

        }

      };

    }  

    componentDidMount() {

      fetch('/meta.json')

        .then((response) => response.json())

        .then((meta) => {

          const latestVersion = meta.version;

          const currentVersion = global.appVersion;  

          const shouldForceRefresh = checkVersionMismatch(latestVersion, currentVersion);

          if (shouldForceRefresh) {

            console.log(`New verion - ${latestVersion}. available need to force refresh`);

            this.setState({ loading: false, isLatestVersion: false });

          } else {

            console.log(`Already latest version - ${latestVersion}. No refresh required.`);

            this.setState({ loading: false, isLatestVersion: true });

          }

        });

    }  

    render() {

      const { isLoading, isLatestVersionAvailable, clearCacheAndReload} = this.state;

      return this.props.children({ isLoading, isLatestVersionAvailable, clearCacheAndReload});

    }

  }

  export default HandleCache;

We can use this HandleCache component to control the app component render 

import HandleCache from './HandleCache';

class App extends Component {

    render() {

      return (

        <HandleCache>

          {({ isLoading, isLatestVersionAvailable, clearCacheAndReload}) => {

            if (isLoading) return null;

            if (!isLoading && !isLatestVersionAvailable) {

              // You can decide how and when you want to force reload

              clearCacheAndReload();

            }  

            return (

                <div className="App-header">

                  <h1>Cache handling - Example</h1>

                  <p>

                   App version - <code>v{global.appVersion}</code>

                  </p>

                </div>

            );

          }}

        </HandleCache>

      );

    }

  }

  export default App;

Force clear cache and hard reload site if there’s a version mismatch

Every time when our app is loaded on the browser we check for the latest version.

And we are going to clear the cache and reload the site depending on the version mismatch.

Note:- some time the above approach also fails because in some cases browser caches the meta.json file and if we request for the file meta.json by fetch(‘/meta.json’)

It picks the meta.json file from the browser cache because this file is already cached and according to the browser if the file is already available you don’t need to communicate with the server. At the end, our whole implementation will not come up with the behaviour that we are expecting.

To avoid this problem we need to do some changes in fetching meta.js – 

fetch(/meta.json?${new Date().getTime()}, { cache: 'no-cache' })

this change will help to solve the problem.

 

Leave a Reply