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} />;
}
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"
}
As part of our build command, we'll also call && npm run git:generate-hash
and place it into our publicly accessible directory. Now, we simply need to poll this file at regular intervals to check for updates and refresh 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 provider so all our instances of our SuperLink
can 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>
);
};
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 core logic is encapsulated within GitHashProvider
. Here, we define our state and establish an interval to periodically fetch 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} />;
}
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. Imagine a user who leaves a tab open overnight and returns to SomeCoolWebApp.xyz
the next day. Unbeknownst to them, the developers have deployed a crucial bugfix. Now, if the user clicks on any of these 'smart' links, they will receive the updated code. They might notice a brief page reload, but this should be infrequent enough to not be disruptive.