What problem are we trying to solve?
Every time a build is created and deployed, the user has to do a hard refresh of the webpage to clear the cache to view the new changes done to the app. This is not a practical solution to ask the user to do it
Our Goal:
I wanted a solution wherein every time I create a new build and deploy it to production, on a basic refresh of the page the user should be able to view the new changes.
We will create a new app by using create-react-app. I will give the app name as clear-cache-app
npx create-react-app clear-cache-app
We will install moment
library as well. You will understand its importance at a later stage.
cd clear-cache-app
npm i moment
Once all the packages are installed, test run the app once
npm start
In package.json
file add the following code at the end of the file
"buildDate": ""
Create a new file update-build.js
. It should reside in the main folder besides package.json
.
update-build.js
will have following code:
const fs = require("fs");
const filePath = "./package.json";
const packageJson = JSON.parse(fs.readFileSync(filePath).toString());
packageJson.buildDate = new Date().getTime();
fs.writeFileSync(filePath, JSON.stringify(packageJson, null, 2));
const jsonData = {
buildDate: packageJson.buildDate,
};
const jsonContent = JSON.stringify(jsonData);
fs.writeFile("./public/meta.json", jsonContent, "utf8", function (error) {
if (error) {
console.log("An error occured while saving build date and time to meta.json");
return console.log(error);
}
console.log("Latest build date and time updated in meta.json file");
});
Whenever a new build is generated we will call this file. What we are doing in update-build.js
is two main things:
- We are generating a current date/time value in epoch.
- We are updating that value in meta.json file. This file will be automatically generated every time a new build is created.
Now update your build command in package.json
file as below:
"build": "node ./update-build.js && react-scripts build",
Next, we will create a higher-order Component (HOC) called withClearCache. Our main App component will be passed as an argument to the withClearCache. The idea here is that before our content of App gets loaded into the browser we need to check whether the our content is latest or not.
We will create a new file into the src
folder named ClearCache.js
with the following code:
import React, { useState, useEffect } from "react";
import packageJson from "../package.json";
import moment from "moment";
const buildDateGreaterThan = (latestDate, currentDate) => {
const momLatestDateTime = moment(latestDate);
const momCurrentDateTime = moment(currentDate);
if (momLatestDateTime.isAfter(momCurrentDateTime)) {
return true;
} else {
return false;
}
};
function withClearCache(Component) {
function ClearCacheComponent(props) {
const [isLatestBuildDate, setIsLatestBuildDate] = useState(false);
useEffect(() => {
fetch("/meta.json")
.then((response) => response.json())
.then((meta) => {
const latestVersionDate = meta.buildDate;
const currentVersionDate = packageJson.buildDate;
const shouldForceRefresh = buildDateGreaterThan(
latestVersionDate,
currentVersionDate
);
if (shouldForceRefresh) {
setIsLatestBuildDate(false);
refreshCacheAndReload();
} else {
setIsLatestBuildDate(true);
}
});
}, []);
const refreshCacheAndReload = () => {
if (caches) {
// Service worker cache should be cleared with caches.delete()
caches.keys().then((names) => {
for (const name of names) {
caches.delete(name);
}
});
}
// delete browser cache and hard reload
window.location.reload(true);
};
return (
<React.Fragment>
{isLatestBuildDate ? <Component {...props} /> : null}
</React.Fragment>
);
}
return ClearCacheComponent;
}
export default withClearCache;
Let's go through the code in the above file to see what exactly we are doing here:
- We are making an api call to
meta.json
file to access it's content. Browsers don't cache the api calls, so even if our files are cached we will always get the latest response from it. ```javascript
useEffect(() => {
fetch("/meta.json")
.then((response) => response.json())
.then((meta) => {
const latestVersionDate = meta.buildDate;
const currentVersionDate = packageJson.buildDate;
const shouldForceRefresh = buildDateGreaterThan(
latestVersionDate,
currentVersionDate
);
if (shouldForceRefresh) {
setIsLatestBuildDate(false);
refreshCacheAndReload();
} else {
setIsLatestBuildDate(true);
}
});
}, []);
* As you can see above, in the response we get the build date from `meta.json` file. We pass that value to a function `buildDateGreaterThan` which accepts two arguments the latest build date generated by `meta.json` file and the cached build date which we took from `package.json` file. This function compares the two date/time values, it returns true if the latest build date is greater than the cached build date else false. We are using the `moment` library for date/time comparison.
```javascript
const buildDateGreaterThan = (latestDate, currentDate) => {
const momLatestDateTime = moment(latestDate);
const momCurrentDateTime = moment(currentDate);
if (momLatestDateTime.isAfter(momCurrentDateTime)) {
return true;
} else {
return false;
}
};
- If the lastest build date is greater than the cached build date we will delete the service worker cache, browser cache and do a hard reload.
const refreshCacheAndReload = () => {
if (caches) {
// Service worker cache should be cleared with caches.delete()
caches.keys().then((names) => {
for (const name of names) {
caches.delete(name);
}
});
}
// delete browser cache and hard reload
window.location.reload(true);
};
- If the latest build date and the cached build date are same we don't clear the cache and we load the component. ```javascript
return (
{isLatestBuildDate ? : null}
);
Now I want to display my build date and time. Since the build date generated is in epoch, I have created two utility functions that will help me format the date in *dd-mm-yyyy hh:mm*
```javascript
/**
* Function returning the build date(as per provided epoch)
* @param epoch Time in milliseconds
*/
export const getBuildDate = (epoch) => {
const buildDate = moment(epoch).format("DD-MM-YYY HH:MM");
return buildDate;
};
The final step is to call the Clear Cache Component in App.js
and to display the build date in my UI. I have updated my App.js
as following:
import React from "react";
import logo from "./logo.svg";
import "./App.css";
import packageJson from "../package.json";
import { getBuildDate } from "./utils/utils";
import withClearCache from "./ClearCache";
const ClearCacheComponent = withClearCache(MainApp);
function App() {
return <ClearCacheComponent />;
}
function MainApp(props) {
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>Build date: {getBuildDate(packageJson.buildDate)}</p>
</header>
</div>
);
}
export default App;
So that's it. You only need to run npm run build
command and your build will be generated with new build date time.
The complete source code can be found on my github repo
Credit:
I want to give a shout out to Dinesh for his article on clearing the cache in react apps.
I have taken the core idea of clearing the cache from his article. Do check it out.
Top comments (29)
I don't understand the point of this article. This is done automatically already. The build system outputs the built files using a hash. The index.html uses those to load it. When you push new code up, the index.html is replaced with updated references. Set the index.html to never be cached and add the new build files alongside the old ones and people going to the site initially get the new version and people already on the page will continue to work and then when they refresh they get the new version as well.
When your project requires to implement strict security policies in some cases it is necessary to disable hashes, for this reason we need another option to clear all caches.
however I agree that this method could be shorter, in my opinion only need attach in the serviceWorker.js a console.log with current date
Hi Arturo
I'm extremely sorry for such a late reply. Can you help me out how I can make this code shorter ?
Of course, as I commented only need attach a console log in your service worker, for example:
Create a file updateBuildTime.js
then update your package file with
With these lines you can add a console.log, I have used the current date to ensure a new value is different to the last version, but you can use another value
The reason of this is very simple, own service worker update all cache our app but need at least one change to download the new version of this file, the console.log with a date is perfect, it will be diferent in each build.
Thanks Arturo, I will check this out.
Hi Eddie
I'm very sorry for such a late reply. The point of this article was, whenever I was generating the build, I was getting the same hash files through create-react-app. It seems to have changed now. I'm getting different has files generation during the build.
Can you share me some example on how to "Set the index.html to never be cached" only on new builds ? It will help us all
Awesome! Finally, I've found this solution. I wonder how every SaaS company could manage to work without this solution. I'm very curious why there has been no simple solution for this important problem in the React eco-system.
Hi Ammar great article, I need you help here, I tried Dinesh approach, but my app is indefinitely reloading, pls guide here, happy to share more info.
Hi Pragz, can you create a small poc with issue share it me via github. I will have a look at it and get back to you.
One thing to check is that my app is not build using create-react-app, so do not have any Service worker at all and hence not being registered, still the below code will be able to work, also my app uses react-router.
Also is it possible to share your email / twitter , for further 1-1 discussion, thanks for the support.
@Pragz : Please email me at ammartinwala52@gmail.com. We can connect over there
I ran the above code and it is working perfectly fine at my end.
Sent the referral mail!
I am also facing the same issue as it goes under the infinite loop please guide me how fix this issue
window.location.reload()
Hi Aman
I'm extremely sorry for such a late reply. If you are still facing the issue, please email to me your code at ammartinwala52@gmail.com
Can you please check once again your implementation? The react web app gets reload only because of withClearCache's useEffect. The code is never going in refreshCacheAndReload. And also, if intentionally, I wanna get the code fall in refreshCacheAndReload function, It lets the UI into never ending rerendering. Please check your implementation and revert me back.
FYI, your buildDate in package.json and meta.json are always going to be same and if they are going to be same, then if (momLatestDateTime.isAfter(momCurrentDateTime)) {
is always going to give you false.
Hi Vishal
I'm very sorry for such a late reply. Can you check the answer to Khorengrig that I just mentioned. It explained what exactly happens.
If you are still not clear then please email me ammartinwala52@gmail.com
Thanks again for this article. I implemented this approach, but the first time my page loads, I can still see the old version. After closing the tab and reopening it I can see the new version. Is this what is suppose to be happening?
Never mind! That happened the first time after the first change. Then every time a change something I can see the most recent version live!!! Thanks a million sir
Hi Alexis
I'm very sorry for such a late reply. I'm glad that I could be of help. Keep writing. Cheers.
Hi Ammar, Thank you so much for the post. I'm trying to get it to work for me but I'm getting the errors in the pic I provided. Can you please help me if you don't mind. Thanks again for the post!!!
Who can explain me what is going in this file. update-build.js. Every time you run build command your package.json buildDate and meta.json buildDate will be the same(You are inserting the same value). Which way this code will work?
Hi Khorengrig
Im very sorry for this very very late reply.
Both dates are generated at the same time, and when you generate your build and deploy, at that point of time the api call from meta.json will fetch the latest build date and it will be compared with package.json date. If package.json date is cached it will be a mismatch and then only the refresh will happen. If your server/deployment configuration is such that it always takes the latest build then your package.json buildDate and meta.json buildDate will always be the same.
I hope you got the point.
Looks like there is a react-cache-buster npm package that is basically the same as this. It can be found here: npmjs.com/package/react-cache-buster.
Hi Brian
I'm very very sorry for such a late reply. Thanks for sharing I will check that out.
It's calling API without login. How I can solve this problem
Hi Jasim
I'm very very sorry for late reply. You would have to place some kind of authentication check first if you want to call the api. If auth is successful then load the ClearCache component which in turns call the API.
How would you integrate the ClearCache component if App.js is a class Based component? Thanks in advance
Hi Alexis, you can refer to the App.js code and CacheBuster code in this article by Dinesh
dev.to/flexdinesh/cache-busting-a-...
Here the implementation is class-based component. Let me know if you have any queries.