Our SaaS CMS/CRM/AMS product Mition is only $199 a month including licenses and hosting!

React CachBuster infinite loop problem - solved (.Net Core 2.2)


Written by Brett Andrew 26th Feb 2020

Credit : The original code for this was posted by  Dinesh Pandiyan
here:

https://dev.to/flexdinesh/cache-busting-a-react-app-22lk

But when testing this I found I was getting a recursive loop and it was really frustrating.

In the end there are 3 parts to solve this problem for React and .Net Core 2.2

Part I - The react CacheBuster

The code below shows my current production version of this code on this site.

I setup timeouts and console logs, to try testing the different order of firing and ensuring everything completed first.

(see the above link from Dinesh for further instructions on how to use this component in your React App.js file)

This is the only file I changed in his solution.

import React from 'react';
import { Utilities } from './../Utilities/Utilities';
//import unregister from './../../registerServiceWorker';

export default class CacheBuster extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            loading: true,
            isLatestVersion: false,
            refreshCacheAndReload: ()  =>{




                //if we get stuck in a loop, this will ensure that refresh occurs before cache delete at some point
                var timeout1 = 100; // clear cache
                var timeout2 = 6000; // refresh browser
                //var timeout3 = 3000; // unregister


                if (caches) {
                     setTimeout(function () {
                        // Service worker cache should be cleared with caches.delete()

                         console.log('Refresh Cache : Step 1');

                         if (caches) {

                             console.log(caches.keys().length + ' keys found');

                            caches.keys().then(async(names) => {
                                // eslint-disable-next-line no-restricted-syntax
                                for (var _i = 0, names_1 = names; _i < names_1.length; _i++) {
                                    var name_1 = await names_1[_i];
                                    await caches.delete(name_1);
                                    console.log(name_1 + ' keys removed');
                                }
                            });
                        }
                    }, timeout1);
                }

;

                setTimeout(function () {
                    
                    console.log('Refresh Cache : Step 2');
                        // clear browser cache and reload page                
                        window.location.reload();
                    
                }, timeout2);

               

                //setTimeout(function () {
                //    console.log('Refresh Cache : Step 3');
                //    // clear browser cache and reload page                
                //    unregister();

                //}, timeout3);
                
                
            }
        };
    }

    // version from `meta.json` - first param
    // version in bundle file - second param
    semverGreaterThan = (versionA, versionB) => {

        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());
            // eslint-disable-next-line no-continue
            if (a === b) continue;
            // eslint-disable-next-line no-restricted-globals
            return a > b || isNaN(b);
        }
        return false;
    };

    componentDidMount() {
        var timestamp = Utilities.timestamp();
        fetch('/meta.json?' + timestamp)
            .then((response) => response.json())
            .then((meta) => {
                const latestVersion = meta.version;
                const currentVersion = global.appVersion;

                const shouldForceRefresh = this.semverGreaterThan(latestVersion, currentVersion);
                if (shouldForceRefresh) {
                    console.log(`We have a new version - ${latestVersion}. Should force refresh`);
                    this.setState({ loading: false, isLatestVersion: false });
                } else {
                    console.log(`You already have the latest version - ${latestVersion}. No cache refresh needed.`);
                    this.setState({ loading: false, isLatestVersion: true });
                }
            });
    }

    render() {
        const { loading, isLatestVersion, refreshCacheAndReload } = this.state;
        return this.props.children({ loading, isLatestVersion, refreshCacheAndReload });
    }
}


The above isn't the only thing you need to do.


Part II - React service workers

In React, the service worker also caches, so you need to comment this out completely from the index.js (there are side effects to not running a service working, like apps, if there is no network connection then it would be an advantage to have this app working still, but having the correct version is more important in my situation.

Goto /ClientApp/Index.js and comment out one line of code!


//this code is at the bottom of index.js in your React web app
//registerServiceWorker();

Part III - .Net Core Cache Policy for SPA files

I feel like this part alone is the key culprit, it doesn't matter how many times you tell the browser to reload if the .net cache policy has already told the browser to not bother getting a new version of this file for a whole day or worse a year.

These are the parts that were required to adjust the policy and are in the root .net core Startup.cs file


  public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {

 app.UseStaticFiles(new StaticFileOptions
            {
                OnPrepareResponse = ctx =>
                {
                    const int durationInSeconds = 60 * 60 * 24; //1 day
                    ctx.Context.Response.Headers[Microsoft.Net.Http.Headers.HeaderNames.CacheControl] =
                        "public,max-age=" + durationInSeconds;
                }
            });
            app.UseSpaStaticFiles(new StaticFileOptions
            {
                OnPrepareResponse = ctx =>
                {
                    if (ctx.Context.Request.Path.StartsWithSegments("/static"))
                    {
                        var headers = ctx.Context.Response.GetTypedHeaders();
                        headers.CacheControl = new Microsoft.Net.Http.Headers.CacheControlHeaderValue
                        {
                            Public = true,
                            MaxAge = TimeSpan.FromDays(1)
                        };

                     
                    }
                    else
                    {
                        // Do not cache explicit `/index.html` or any other files.
                        var headers = ctx.Context.Response.GetTypedHeaders();
                        headers.CacheControl = new Microsoft.Net.Http.Headers.CacheControlHeaderValue
                        {
                            Public = true,
                            MaxAge = TimeSpan.FromDays(0)
                        };
                    }
                }
            });


...

}





This part was already required a bit further down

app.UseSpa(spa =>
            {
                spa.Options.SourcePath = "ClientApp";

                spa.Options.DefaultPageStaticFileOptions = new StaticFileOptions()
                {
                    OnPrepareResponse = ctx => {
                        // Do not cache implicit `/index.html`.  See also: `UseSpaStaticFiles` above
                        var headers = ctx.Context.Response.GetTypedHeaders();
                        headers.CacheControl = new Microsoft.Net.Http.Headers.CacheControlHeaderValue
                        {
                            Public = true,
                            MaxAge = TimeSpan.FromDays(0)
                        };
                    }
                };


Now testing this is difficult. Testing Rules:

You have to get this version into a hosted dev/staging environment, you can't really test it locally as the manifest / meta.json aren't used locally.

Once it is in dev/staging you can load up a few pages, mobiles with the page, update the version and then test it out. But you also have to be aware that the browser might have the index.js file and whatever the old cache policy was (for me it was a day, but for others if it is set to something longer like a year, it could be difficult to test how it will impact your users.

The one thing you want to test fully is that it does not cause any loop redirects, which if your old cache policy is long like a year - it will.  If you have set a long cache policy then apply parts II and III now, but leave part I until you are sure your users cache policy for the index.js file is 0 days.


Contact me here if you have anything else to add!

Contact us