Refresh Your React App Discretely
Joshua Pohl
Posted on May 14, 2022
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. 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 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 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} />;
}
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.
Posted on May 14, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.