How to Cache bust web app
Tags: webdev, javascript, react, devops
Published: 2022-06-05 18:13:20


## Why do we need to do cache busting ?
Static file gets cached and can be stored for a long period of time before it ends up expiring. So in order to get the latest *features* and *fixes* we need to remove the cache so that browsers can get the latest updates.

Additionally, have you ever felt **web application** like `soundcloud`, `facebook` etc..., asking to reload or upgrade after deployment, no right ? how the hell are they doing the updates ?

---

### Making sure we have cache invalidated in CDN

Cache busting solves the CDN caching issue by using a unique file version identifier.
As discussed in the [previous article](https://dev.to/mnathani/how-to-cache-frontend-app-1kkj) we used `no-cache` policies on some files to bust the cache using unique files names. Hence we are sure that `cloudfront` will always keep the updated files..

---

### Lets bust the browser cache
So today will cover what is the one of the best approach to seamlessly bust the cache of the frontend application in the browsers when a deployment is done, without the user feeling the app was upgraded...

#### The trick
The trick is we keep updating the version of the application in `meta` file, which **never** gets cached... and to seamlessly upgrade, we perform a **reload** on the **route change** so that user would feel as if they are redirecting to a different view, but in our case we are actually cache busting our application to get the new update from the `build` we deployed.

Let's dig in to see how its possible.


#### How to check if a new build is generated
To know when we have a new build in browsers we keep two version of the application.

- `package.json` version
- `meta.json` version

#### What are these version and how we manage it

Here is the command `prebuild` that runs before every `build` to manage both versions as shown below:

```js
    "release": "HUSKY=0 standard-version",
    "prebuild": "npm run release && node ./build-version",
```

- `package.json` version is kept and maintain using tools like [Semantic versioning](https://semver.org/) or [Standard release](https://github.com/conventional-changelog/standard-version) which upgrades the package version after every deployment. Here we are using `standard-version` to always get our `package.json` upgraded. 

- `meta.json` version is created in `public` folder using a script `build-version.js` we wrote to make sure we always get a latest version after deployment.

`build-version.js`:
```js
const fs = require('fs');
const { version } = require('./package.json');

fs.writeFile('./public/meta.json', JSON.stringify({ version }), 'utf8', (error) => {
  if (error) {
    console.error('Error occurred on generating meta.json:', error);
    return;
  }
  // eslint-disable-next-line no-console
  console.info(`meta.json updated with latest version: ${version}`);
});
```

The above scripts takes the latest `version` from `package.json` which was upgraded using `npm run release` and save it to `meta.json` using [fs.writeFile](https://nodejs.dev/learn/writing-files-with-nodejs).

Here is how the output of the above script will look like:

`meta.json`:

```json
{ "version": "108.0.0" }
```

Before we proceed to the next step, let me inform you that we are using the following frameworks in our app:
- Reactjs: [react](https://reactjs.org/)
- Routing:  [react-router-dom](https://v5.reactrouter.com/web/guides/quick-start)


#### Code to check application is upgraded
We created a hook that could be placed in a suitable position in your application, preferably on layouts/routes:

```js
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';
import { version } from '../../../package.json';

const useCacheBuster = () => {
  const location = useLocation();
  const parseVersion = (str) => +str.replace(/\D/g, '');

  useEffect(() => {
    fetch(`/meta.json?v=${+new Date()}`, { cache: 'no cache' })
      .then((response) => response.json())
      .then((meta) => {
        if (meta?.version) {
          const metaVersion = parseVersion(meta.version);
          const packageVersion = parseVersion(version);
          if (packageVersion < metaVersion) {
            if (window?.location?.reload) {
              window.location.reload();
            }
          }
        }
      })
      .catch((error) => {
        console.error('something went wrong fetching meta.json', error);
      });
  }, [location]);

  return null;
};

export default useCacheBuster;
```

The hooks above is doing the following:

1. [useEffect](https://reactjs.org/docs/hooks-effect.html) having a `deps` of `location`, which runs on every change of the route.
2. `parseVersion` is a pure function that can format the version like `"1.0.5"` into a number `105`, so we can compare the versions.
3. On changing the app route, the hook fires and fetches `/meta.json` files from the root of the app, important thing to note here is we are passing a **date param:** and **cache**, to make sure this [file never returns the cached content on fetching](https://stackoverflow.com/a/9692722/4527878).
4. Using the response of `meta.json` we are checking if `packageVersion` is less than `metaVersion`, which means the new build has deployed and the browser is using the old cached build, so the app needs to reload.
5. If the above condition is `true` then reload it!.  

**NOTE**: if you are using a CDN then you need to cache bust in CDN by adding the `meta.json` to behaviours as shown [here](https://dev.to/mnathani/how-to-cache-frontend-app-1kkj)


**P.S**: we can optimize the fetching of meta, by conditionalizing it on certain routes rather then all.


That's it folks..., all you need to perform cache bust in browser programmatically.   

---

## Conclusion
The solution above is useful for the scenarios when you have a frequent deployment to production.
Moreover, in my understanding for apps used in webview or apps saved on homepage could also be similarly busted with different reload methods... 

---

Please feel free to comment on the approach, would love to hear your feedback on this.

Regards.

{% link https://dev.to/mnathani/plugin-to-make-micro-frontend-using-cra-425n %}