DEV Community

Cache Busting a React App

Dinesh Pandiyan on April 14, 2019

TL;DR - SemVer your app and generate a meta.json file on each build that won't be cached by the browser. Invalidate cache and hard reload the app w...
Collapse
 
eddyp23 profile image
EddyP23

Hi Dinesh, good article.

I was wondering though, why would you need a cache-invalidation mechanism. We are developing with angular, but I believe, you can setup things in the same way with React.

We have also been struggling with browser caching, because SEO loves resources cached for as long as possible. So here is our current setup:

  • js and css files all have a hash part in their filename, so we cache them forever (366 days to be precise), but if they change, the filename changes, so no trouble there.
  • index.html - never cache it. Here we have references to js and css files.
  • fonts, images - we cache them forever (366 days again), but we version them all. Therefore, when we update them, we just bump up the version and cache invalidation solved.

This way, SEO performance score is happy that we cache everything forever, but also, we never have to deal with invalid data.

I am not trying to sell my approach here, I am just honestly curious what is your use case for such cache invalidation mechanism? Thanks.

Collapse
 
flexdinesh profile image
Dinesh Pandiyan

Hey Eddy — Good question.

We definitely need to leverage browser and server cache (PWA service worker implementation for cache control is a lot better).

But there are still a few gotchas we haven't solved yet. These "recommended" ways work most of the time but in some rare cases, they won't as I highlighted at the beginning of the post.

So this technique is more of taking control of cache "within your app code" and use this as a fallback approach when all else fails.

We have a peculiar case where I work — safari web app pinned to the home screen and users won't quit it for a few months (restaurant iPads). We simulated native app behavior with PWAs but cache busting instantly after a new deploy was tricky. This technique eventually helped us.

Collapse
 
davl3232 profile image
David Villamizar • Edited

The service worker that comes with react does cause scenarios where the cache is not busted that feel a bit weird. For that reason it was removed from create-react-app a while ago, as discussed here github.com/facebook/create-react-a...

The mechanism @eddyp23 mentions works perfectly and it's supported by default by create-react-app create-react-app.dev/docs/producti...

Collapse
 
jakeg9191 profile image
Jacob Garlick

@eddyp23 I am using React but as you mentioned the two are probably largely the same in terms of setup. I am wondering how you would set up the caching for js and css files, do those have to be individually referenced in index.html?

Collapse
 
jamesthomson profile image
James Thomson

I think it should be noted the window.location.reload(true) will only disable the cache for the current page. Any subsequent requests (async js/css, other pages, images, etc.) will not be cache busted. It's also very dependant on the browser. Chrome for example hangs on to cache for dear life unless instructed by headers to do otherwise. The best way to ensure cache is busted is to fingerprint filenames based on the files content and to never cache pages (e.g. index) so references remain fresh.

Collapse
 
flexdinesh profile image
Dinesh Pandiyan • Edited

James — That's very true. Browsers sometime decide to ignore window.location.reload(true).

But caches.delete() will always delete the cache. So reloading synchronously after cache.delete() should clear the cache for the user.

Collapse
 
mattacus profile image
Matt

You mention reloading synchronously... I was having issues with infinite looping when our app updated and it seems due to window.location.reload firing before our caches had time to clear, in the code you shared for CacheBuster. Simply adding aPromise.all() on the caches.delete() promises solved the issue in our case.

Thank you for taking the time to share this!

Thread Thread
 
assassin profile image
a-ssassi-n • Edited

Hi Matt,

We are facing the same issue, can you please let me know the exact code you have used to solve this issue?

Something like this? Please correct me if I am wrong.

caches.keys().then(async function(names) {
await Promise.all(names.map(name => caches.delete(name)));
});

Thread Thread
 
mattacus profile image
Matt

Yep, I used promises but that's pretty much exactly what I did, then do the window.location.reload after your await

Thread Thread
 
assassin profile image
a-ssassi-n

Thanks, I got it now.

Thread Thread
 
laura6739 profile image
Laura6739 • Edited

Hi a-ssassi-n,
I'm having the same issue and I tried to do it the same way as this:
refreshCacheAndReload: () => {
if (caches) {
caches.keys().then(async function(names) {
await Promise.all(names.map(name => caches.delete(name)))
})
}
window.location.reload(true)
},
And it keeps happening did I miss something?, can you please give me any guide?

Thread Thread
 
louvki profile image
Lukas Vis
if (caches) {
  const names = await caches.keys();
  await Promise.all(names.map(name => caches.delete(name)));
}
window.location.reload();
Collapse
 
kilsen profile image
Kevin Ilsen

This is a really great solution and very well explained in this article! There's just one problem: location.reload(true) is deprecated! Calling location.reload() is still in the spec, but passing true to force the browser to get the page again and bypass the cache is deprecated. And because it's deprecated, some browsers are starting to ignore it. As a result, we are seeing an infinite loop where our browser keeps reloading the same page from cache. Have you thought about an alternative that will achieve the same result?

Collapse
 
kulshrestha97 profile image
Rajat Kulshreshtha

Hey, any updates on this?

Collapse
 
ammedinap93 profile image
ammedinap93

Hi Dinesh, thanks for the great article. I found it very helpful and I use it for a project my team is working on. At a point, you say: "It won't be cached by the browser as browsers don't cache XHR requests." Well, for some reason my browser is catching it.
I will try
fetch(/meta.json?${new Date().getTime()}, { cache: 'no-cache' })
as @ppbraam suggested dev.to/ppbraam/comment/gdac
If you have any other idea that may help I would appreciate it. Thanks in advance.

Collapse
 
sabikeraja profile image
Sabike-Raja

Hi Dinesh,
In my case the fetch('/meta.json') does not work. How it will tyake the meta json from public folder.

Collapse
 
pragziet profile image
Pragz

Hi, are you able to figure out meta.json file, I am also facing the same issue ::

Failed to load resource: the server responded with a status of 404 (Not Found)

Cannot GET /meta.json

Collapse
 
briang123 profile image
Brian Gaines

What web server are you fetching from? I noticed the same on IIS as asp.net core protects json and thinks they're a config file so serves up a 404. Changing the file extension to txt (meta.txt) resolved that issue.

Collapse
 
irreverentmike profile image
Mike Bifulco

This is stellar. Looking forward to giving this a shot. Thank you Dinesh!

Collapse
 
gravitymedianet profile image
David Tinoco • Edited

Dinesh, how would this work with a functional component? I tried to translate it but it just keeps refreshing over and over. Here is my FC version, using Typescript

import { observer } from "mobx-react";
import React, { useEffect, useState } from "react";
import { useStores } from "../../contexts";

const semverGreaterThan = (versionA: string, versionB: string) => {
    const versionsA = versionA.split(/\./g);
  const versionsB = versionB.split(/\./g);
  while (versionsA.length || versionsB.length) {
    const a = Number(versionsA.shift());
    const b = Number(versionsB.shift());
    if (a === b) continue;
    return a > b || isNaN(b);
  }
  return false;
}

const CacheBuster: React.FC<any> = ({ children }) => {

    const store = useStores();
    const [loading, setLoading] = useState(true);
    const [isLatestVersion, setIsLatestVersion] = useState(false);

    const refreshCacheAndReload =  () => {
      console.log('Clearing cache and hard reloading...')
      if (caches) {
        // Service worker cache should be cleared with caches.delete()
        caches.keys().then(function(names) {
          for (let name of names) caches.delete(name);
        });
      }
       // delete browser cache and hard reload
      window.location.reload();
    };

    useEffect(()=>{
        fetch('/meta.json')
        .then((response) => response.json())
        .then((meta) => {
          const latestVersion = meta.version;
          const shouldForceRefresh = semverGreaterThan(latestVersion, store.appVersion);
          if (shouldForceRefresh) {
            console.log(`We have a new version - ${latestVersion}. Should force refresh`);
            setLoading(false);
            setIsLatestVersion(false);
          } 
          else {
            console.log(`You already have the latest version - ${latestVersion}. No cache refresh needed.`);
            setLoading(false);
            setIsLatestVersion(true);
          }
        })
        .catch(err=>{
            console.log('App could not be loaded. Check if meta.json file has been generated.');
        });
        //eslint-disable-next-line
    }, []);

    return children({ loading, isLatestVersion, refreshCacheAndReload });
}

export default observer(CacheBuster);
Enter fullscreen mode Exit fullscreen mode
Collapse
 
nicks78 profile image
Nicolas

Hi Dinesh,

Good work.

I am having an issue when the metal.json version not match package.json version it goes to infinit loop because the meta.json is not updated.

Any suggestion ?

Thanks

Nic

Collapse
 
mandrecorrea5 profile image
Marcos Correa

I had a same problem. Is this code ever the versions will differents, the package.json version and meta.json version. In this case, how I update the meta.json version after realoading and cleaning cache?

Collapse
 
flexdinesh profile image
Dinesh Pandiyan

If you could create a sample repo that reproduces the problem I can take a look and see what's happening.

Collapse
 
nicks78 profile image
Nicolas

OK I will try.

I also got this error when i manually refresh the page after new version has been updated in server :

service-worker.js:1 Couldn't serve response for http Error: The cached response that was expected is missing.

Collapse
 
amanchaudhary1998 profile image
AmanChaudhary1998

// While Implementing these code I am stuck into the infinite loop
// can anyone help me to where I am commit the mistake
if (caches) {
const names = await caches.keys();
console.log(names);
await Promise.all(names.map((name)=>caches.delete(name)))
}
window.location.reload();

Collapse
 
amanchaudhary1998 profile image
AmanChaudhary1998

While Implementing these code I am stuck into the infinite loop
can anyone help me to where I am commit the mistake
if (caches) {
const names = await caches.keys();
console.log(names);
await Promise.all(names.map((name)=>caches.delete(name)))
}
window.location.reload();

Collapse
 
amanchaudhary1998 profile image
AmanChaudhary1998

<if (caches) {

            const names = await caches.keys();

            console.log(names);

            await Promise.all(names.map((name)=> caches.delete(name)))

        }
        window.location.reload();
Enter fullscreen mode Exit fullscreen mode

These will fall into infinite loop can any one help me how to solve these issue>

Collapse
 
amanchaudhary1998 profile image
AmanChaudhary1998

if (caches) {

            const names = await caches.keys();

            console.log(names);

            await Promise.all(names.map((name)=> caches.delete(name)))

        }
        window.location.reload();
Enter fullscreen mode Exit fullscreen mode

These will fall into infinite loop can any one help me how to solve these issue

Collapse
 
amanchaudhary1998 profile image
AmanChaudhary1998

/*While Implementing these code I am stuck into the infinite loop
can anyone help me to where I am commit the mistake */
if (caches) {
const names = await caches.keys();
console.log(names);
await Promise.all(names.map((name)=>caches.delete(name)))
}
window.location.reload();

Collapse
 
ppbraam profile image
ppbraam

Thank you for this solution!

If for some reason your cache still won't clear, maybe this will help:

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

Collapse
 
pherm profile image
pherm

I have same problem with rca, it don't work well. I always have same meta.jon version, is it normal?

Collapse
 
asimhussain profile image
asimhussain

I am new to caching and as per this article, this solution of deleting a stale cache can be achieved without a refresh? My understanding is EVEN with a refresh, some applications are cached, hence various solutions used to invalidate cache.

In my react app, if I refresh my app, the latest code is reflected. It's only IF the code is not refreshed and I go to another page, nothing is updated.

I am also using react router. Does an internal request need to be made for meta.json then?

Collapse
 
pragziet profile image
Pragz • Edited

Hi Dinesh, My app is not being created using create-react-app, so Service Worker is missing there, so this approach may not work, but then I am not sure how can I clear the browser cache programmatically for user to see the updates, happy to share more information if any one willing to help, thanks in advance.

Collapse
 
sumanyusoniwal profile image
sumanyu-soniwal • Edited

Amazing article. Worked like a charm.

caches.keys().then(async function(names) {
await Promise.all(names.map(name => caches.delete(name)));
});

This helped in the extension of the entire article. Thanks a lot, Dinesh.

Collapse
 
brettandrew profile image
bretta99999

Awesome work Dinesh, this really helped our project fix the caching issues we were having!

Collapse
 
runecreed profile image
Runecreed

Hey Dinesh

Thanks for this article, really helpful and clear, worked like a charm and easily slotted into the build process!

Collapse
 
gadeoli profile image
Gabriel Morais • Edited

"caches" variable? where it came from?

if (caches) {
      const names = await caches.keys();
      await Promise.all(names.map(name => caches.delete(name)));
}
Enter fullscreen mode Exit fullscreen mode
Collapse
 
tvanantwerp profile image
Tom VanAntwerp

How does this compare to using service workers for offline caching?

Collapse
 
flexdinesh profile image
Dinesh Pandiyan

Leveraging Service workers for controlling your cache should be the de-facto first approach. Service workers are pretty great and hashing every new build is a life saver. But there are situations where we will need custom cache control within the code. This approach will help in such situations.

More info in this comment — dev.to/flexdinesh/comment/a79c

Collapse
 
akshay5995 profile image
Akshay Ram Vignesh

Just what I was looking for! Thanks Dinesh! 😁

Collapse
 
repomangit profile image
repoman

Nice one Dinesh, thanks for taking the time to prepare this.

Collapse
 
sudhirkumarau3 profile image
Sudhir Kumar

I am facing issues implementing it. My application went into an infinite reloading state. Though I tried using Promise.all as suggested by @a-ssassi-n but it didn't work for. Can anybody help me with this?

Collapse
 
kulshrestha97 profile image
Rajat Kulshreshtha

Hey did you get the solution?

Collapse
 
jasimur profile image
Jasim Uddin

After implement this It's calling API without login which is doesn't make sense, is there any solutions?

Collapse
 
abdullah727 profile image
abdullah727 • Edited

Hi,
i am facing an issue on production. Unable to access /meta.json endpoint..working fine on local.
getting this error: SyntaxError: Unexpected token < in JSON at position
Regards
Abdullah Saud

Collapse
 
takhine profile image
Aniketh Nair • Edited

I had the same issue, this change helped me

fetch('/meta.json',{
headers : {
'Content-Type': 'application/json',
'Accept': 'application/json'
}
})

Collapse
 
shauntrennery profile image
Shaun Trennery

@abdullah727 Did you ever manage to get the "Unexpected token

Collapse
 
abdullah727 profile image
abdullah727

Remember to push your commit with --tag attribute — git push origin master --tags
what does this line means and why it is important?

Collapse
 
gloubier profile image
gloubier

I was wondering the same thing. Is it supposed to change anything?

Collapse
 
harshav198 profile image
harsha

it keeps rerendering

Collapse
 
saeed_padyab profile image
Saeed Padyab • Edited

Hello Dinesh, amazing article.

Collapse
 
jinog268 profile image
jinog268

I have implemented using the above method. But there was infinite loop refreshing problem. What is the solution for this?

Collapse
 
gravitymedianet profile image
David Tinoco

Dinesh, how do you handle the meta.json during development, with hotloading?

Collapse
 
aeharake profile image
Ahmad El Harake

Thanks man, I'll try your solution and let you know. Interesting article keep it up!

Collapse
 
yairifrach profile image
yairIfrach

Remember to push your commit with --tag attribute — git push origin master --tags
what does this line means and why it is important

Collapse
 
arung86 profile image
Arun Kumar G • Edited

Its because with each version update the code will be tagged and you need to have these tags in the remote too, otherwise you might end up creating duplicate version

Collapse
 
buvaneswarirk profile image
BuvaneswariRK • Edited

Hi Dinesh, There is a feature that uuid generation for the filename to avoid cache issue by default, will that be effective when compared to your solution?

Collapse
 
firouzyar profile image
alireza firouzyar

Hi Dinesh

I have problem with infinit reloading even with Promise
dose any one fixed the infinit reloading?