DEV Community

Joshua Pohl
Joshua Pohl

Posted on • Edited on • Originally published at lightpohl.me

Refresh Your React App Discretely

One of the hurdles introduced by single-page apps is that users can go much longer without being updated to the latest deployed code. This affects not only custom React setups but even more opinionated options like Next.js. In a perfect world, APIs should be backwards compatible and fail gracefully when something is missed, but there's no doubt in my mind that a user with a client bundle that is several days old will be more likely to run into issues. Fortunately, there's an easy way we can update our client app with the user being none the wiser. We'll build our example with React and React Router, but the concepts apply to all client JavaScript frameworks.

Links And Anchors

The main reason users can have much longer running sessions without receiving new JavaScript is because of the nature of single-page applications. Single-page applications often utilize client-side routing, which means the full page will not be refreshed: the app will instead fetch data it needs for the next page and manipulate the browser history manually without requesting the full HTML. We could just not use client-side routing, but we will lose a lot of that speediness we associate with these feature-rich web applications. What if we could fall back to native anchors only when necessary?

function SuperLink({ href, ...other }) {
  const { shouldUseAnchor } = useSomeFunction();

  if (shouldUseAnchor) {
    return <a href={href} {...other} />;
  }

  // a React Router <Link />
  return <Link to={href} {...other} />;
}
Enter fullscreen mode Exit fullscreen mode

This code looks promising. But how can we calculate shouldUseAnchor to determine which type of link to render?

git.txt

One simple option is to expose a text file with a Git hash that is generated from our source code. Wherever we expose our fonts and possible images (e.g. /static), we can place git.txt at build-time.

{
  "git:generate-hash": "git ls-files -s src/ | git hash-object --stdin > static/git.txt"
}
Enter fullscreen mode Exit fullscreen mode

As part of our build command, we'll also call && npm run git:generate-hash and place it into our publicly accessible directory. All we need to do now is poll for this file on a fixed interval to check for new updates and update our SuperLinkcomponent.

GitHashProvider

Any page could have a number of links on it — it would be mistake to have each instance poll for our hash file. Instead, we'll wrap our app in a React context provider so all our instances of our SuperLinkcan use it.

import * as React from 'react';

// Some boilerplate to prepare our Context
const GitHashContext = React.createContext({
  hash: '',
  hasUpdated: false
});

// Setup our hook that we'll use in `SuperLink`
export const useGitHash = () => React.useContext(GitHashContext);

// Function used to actually fetch the Git hash
const TEN_MINUTES_IN_MS = 60000 * 10;
async function fetchGitHash() {
  let gitHash = '';

  try {
    const result = await fetch('/static/git.txt');
    gitHash = await result.text();
  } catch (error) {
    console.error(error);
  }

  return gitHash;
}

// The provider we'll wrap around our app and fetch the Git hash
// on an interval
export const GitHashProvider = ({ children }) => {
  const [state, setState] = React.useState({ hasUpdated: false, hash: '' });

  const updateGitVersion = React.useCallback(async () => {
    const hash = await fetchGitHash();

    if (hash) {
      setState((prevState) => ({
        hash,
        hasUpdated: !!prevState.hash && prevState.hash !== hash
      }));
    }
  }, []);

  React.useEffect(() => {
    const interval = setInterval(() => {
      updateGitVersion();
    }, TEN_MINUTES_IN_MS);

    return () => clearInterval(interval);
  }, [updateGitVersion]);

  return (
    <GitHashContext.Provider value={state}>{children}<GitHashContext.Provider>
  );
};
Enter fullscreen mode Exit fullscreen mode

That's quite a bit of code, so let's walk through it. We define the boilerplate for context and the hook that will provide access to its data (GitHashContext and useGitHash). Next, we define a simple wrapper around fetch that will query our git.txt and pull out the hash.

The meat of the logic is in GitHashProvider and it's not too bad. We define our state and kick off an interval that will run every ten minutes and grab the latest Git hash. If we have already saved a Git hash before and it's different than the latest, we'll set hasUpdated to true. We keep track of the previous hash for later comparisons. We're now ready to use it in SuperLink!

function SuperLink({ href, ...other }) {
  const { hasUpdated: hasGitHashUpdated } = useGitHash();

  if (hasGitHashUpdated) {
    return <a href={href} {...other} />;
  }

  // a React Router <Link />
  return <Link to={href} {...other} />;
}
Enter fullscreen mode Exit fullscreen mode

When to Use It

Depending on the application, the locations where you'd want to use our new SuperLinkcould change. Personally, I feel that links in your header are almost always good candidates. Let's imagine the flow as the end-user, we've left a tab open overnight and return to SomeCoolWebApp.xyz. Unknown to us, the devs have deployed a really important bugfix in code that we'll now receive if we click on any of these "smart" links. The user might notice a quick flash as the full page loads on navigation, but this should happen infrequently enough to not really be noticeable.

Top comments (0)