How to Propagate Front-End Updates to End Users

Jollen Moyani - Mar 10 '23 - - Dev Community

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:

  • Your development team fixes a major UI bug in your SPA. The fix involves breaking changes.
  • You, as a team lead, merge the UI fix onto your release or main branch. This triggers deployment on your CI/CD tool—let’s use Vercel for this example.
  • Vercel then builds your newly versioned application, deploys it, and assigns the new deployment to your application domain.

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.

The detection algorithm

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.

A deployment hash on a main.js file

A deployment hash on a main.js file

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.

The hash on the second deployment

The hash on the second deployment

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:

  1. Periodically poll the deployment application to fetch its index.html file. This can be done using the fetch API bundled into Node.js, or an AJAX request which is natively available and does not need any additional libraries to be installed within the SPA.
  2. Compare the new file to the current file opened in the browser.
  3. Take action based on the version change, if any.

Implementation

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 };
}

Enter fullscreen mode Exit fullscreen mode

The previous snippet illustrates the usePoller hook, which implements the following:

  • A state variable, isNewVersionAvailable : The state variable is defined to identify if there is a change between each periodic check. It is returned at the end of the hook, allowing the consumer to use the Boolean to take the actions necessary.
  • An effect: The effect will obtain a text response of the currently deployed version by performing a GET request on the provided deployment URL. It will then obtain the main.HASH.js script file by performing a regex check. Afterward, the main.HASH.js file of the current deployment running on the user’s browser is retrieved using a getElementsByTagName() selector. Finally, it compares the two filenames and updates the state to determine the version bump.

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;

Enter fullscreen mode Exit fullscreen mode

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.

Version update notification

Version update notification

How do you propagate the update?

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.

GitHub reference

The code implemented in this article is available in this GitHub repository.

Conclusion

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!

Related blogs

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .