Refresh Your React App Discretely
Tags: react, javascript, routing, tutorial
Published: 2022-05-14 18:08:19

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](https://nextjs.org/). 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](https://developer.mozilla.org/en-US/docs/Learn/Tools_and_testing/Client-side_JavaScript_frameworks/Introduction#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?

```jsx
function SuperLink({ href, ...other }) {
  const { shouldUseAnchor } = useSomeFunction();
    
  if (shouldUseAnchor) {
    return <a href={href} {...other} />;
  }
  
  // a React Router <Link />
  return <Link to={href} {...other} />;
}
```

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.

```json
{
  "git:generate-hash": "git ls-files -s src/ | git hash-object --stdin > static/git.txt"
}
```

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 `SuperLink `component.

## 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](https://reactjs.org/docs/context.html) provider so all our instances of our `SuperLink `can use it.

```jsx
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>
  );
};
```

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`!

```jsx
function SuperLink({ href, ...other }) {
  const { hasUpdated: hasGitHashUpdated } = useGitHash();
    
  if (hasGitHashUpdated) {
    return <a href={href} {...other} />;
  }
  
  // a React Router <Link />
  return <Link to={href} {...other} />;
}
```

## When to Use It

Depending on the application, the locations where you'd want to use our new `SuperLink `could 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.