The front ends of single-page applications (SPA) rely on JavaScript APIs to display page content. Therefore, they do not follow the traditional approach of sending requests to a server to receive new HTML to render. Instead, they return one HTML file, index.html, and use JavaScript APIs to manipulate its content. This process does not involve numerous HTTP calls to request HTML when a route changes.
This helps create highly responsive applications that provide instant responses because everything is handled in the web browser via JavaScript. However, this approach also creates a significant disadvantage.
Consider the following scenario:
Now all new users will receive your newly deployed version as they load the application for the first time. However, if a user left the application open in a separate window during the new deployment and does not refresh the application, they will still experience the bug because the application has no way of identifying a change that has occurred. This is because the application does not request new HTML but uses JavaScript APIs to manipulate the content instead.
This is a unique hurdle in delivering version changes to SPAs. If you’re not prepared for it, some customers may be stuck with the previous buggy version of your application.
This article will provide an in-depth walkthrough of how developers can propagate front-end updates to their end users, ensuring their customers always have the most recent deployment at hand.
In a SPA, the only page served is index.html. Therefore the index.html file must be monitored for changes whenever a new release occurs. If a change is present, that means a new version has been published, and the user must respond to have access to the latest version.
Luckily, a developer does not need to implement any versioning algorithm on the index.html file when they use Webpack (a module bundler for JavaScript that can transform front-end assets). Webpack will always generate a unique hash for each file on each build. This guarantees that each build will create a new index.html file with a unique hash that is part of the file name embedded in the HTML. These hash values can be compared.
As shown in the previous figure, Webpack has added the hash value 4e2842e8 on the main file for this build. However, Webpack will create a new hash if this application is redeployed. This is depicted in the following figure.
As shown in the figure, the hash value on the main.js file has been updated to e622d7cfs on the second deployment. We can then compare the two file names to determine the version change.
To compare the hashes in the file name, you will need to follow these steps:
For this implementation, I will be using a React application. However, you can implement this algorithm in any SPA framework, such as Angular or Vue, as the fundamental rule of creating a new hash on each Webpack build will still apply.
To proceed, we will create a usePoller hook that is responsible for implementing the version bump identifier. The implementation of the hook is illustrated in the following code sample.
import { useEffect, useState } from "react"; const SCRIPT_REJEX_MAIN = /^.*<script.*\/(main.*\.js).*$/gim; export const UsePoller = ({ deploymentUrl }) => { const [isNewVersionAvailable, setIsNewVersionAvailable] = useState(false); useEffect(() => { const compareVersions = async () => { // request the index.html file from the deployment const fetchedPage = await fetch(deploymentUrl, { method: 'get', mode: 'cors' }); // get the text from the response const loadedText = await fetchedPage.text(); // get the main.js file to get hash const matchResponses = SCRIPT_REJEX_MAIN.exec(loadedText); let remoteMainScript = matchResponses.length > 0 ? matchResponses[1] : undefined; if (remoteMainScript === undefined) { console.log("Could not find main script in index.html"); setIsNewVersionAvailable(false); return; } // get the current version hash from current deployment let currentMainScript = undefined; // get text representation of document const scriptTags = document.head.getElementsByTagName('script'); for (let i = 0; i < scriptTags.length; i++) { const scriptTag = scriptTags[i]; currentMainScript = /^.*\/(main.*\.js).*$/gim.exec(scriptTag.src) === null ? undefined : /^.*\/(main.*\.js).*$/gim.exec(scriptTag.src)[1]; } // if the current main script or the remote main script is undefined, we can't compare // but if they are there, compare them setIsNewVersionAvailable( !!currentMainScript && !!remoteMainScript && currentMainScript !== remoteMainScript ); console.log("Current main script: ", currentMainScript); console.log("Remote main script: ", remoteMainScript); } // compare versions every 5 seconds const createdInterval = setInterval(compareVersions, 5000); return () => { // clear the interval when the component unmounts clearInterval(createdInterval) }; }, [deploymentUrl]); // return the state return { isNewVersionAvailable }; }
The previous snippet illustrates the usePoller hook, which implements the following:
Hereafter, the custom hook is invoked in the App component, as shown in the following sample.
import logo from './logo.svg'; import './App.css'; import { UsePoller } from './use-poller'; import { useEffect } from 'react'; const INDEX_HTML_DEPLOYMENT_URL = "https://front-end-version-change-deploy.vercel.app/index.html"; function App() { const { isNewVersionAvailable } = UsePoller({ deploymentUrl: INDEX_HTML_DEPLOYMENT_URL }); useEffect(() => { if (isNewVersionAvailable) { console.log("New version available, reloading..."); } else { console.log("No new version available"); } }, [isNewVersionAvailable]) return ( <div className="App"><header className="App-header"><img src={logo} className="App-logo" alt="logo" /><p> Edit <code>src/App.js</code> and save to reload </p><aclassName="App-link"href="https://reactjs.org"target="_blank"rel="noopener noreferrer" > Learn React </a></header></div> ); } export default App;
The following figure depicts the output of the hook when there is no new deployment and when a new deployment has been made while the previous version is open in the browser.
After implementing the detection algorithm, it is essential to allow your user to take the necessary action to stay up-to-date with your application. For instance, you can perform a force reload or show an alert message.
Typically, the ideal approach is to show an alert message within useEffect via the alert() function rather than forcefully reloading the site, which may cause the user to lose data they did not save.
However, you can determine the correct approach based on your use case. For example, if you are working on a mission-critical application that requires always serving up-to-date versions, it may be best to force reload.
The code implemented in this article is available in this GitHub repository.
This article provided an overview of identifying front-end updates whenever a deployment is made in a single-page application.
I hope you found this article helpful. Thank you for reading!
Syncfusion Essential Studio® for React suite offers over 80 high-performance, lightweight, modular, and responsive UI components in a single package. It’s the only suite you’ll need to construct a complete app.
If you have questions, contact us through our support forum, support portal, or feedback portal. We are always happy to assist you!