Article cover image

Notify users about new deployments with Next.js + SWR

  • Leonardo
    Leonardo
    Senior Engineer

Notifying users about new deployments helps users stay informed about new features and bug fixes, and it can also help improve the user experience by ensuring that users are always using the latest version of the app. In this article, we will discuss how to use Next.js and SWR to notify users about new deployments.

In a rush? Check the full source code here

Version

To store a version, we'll save a JSON file containing the timestamp in the public folder. The version will be saved before each build with a prebuild script, so that we can read that file in the app to tell which version the app is using when opened.

// scripts/create-version-json.mjs

import { writeFile } from 'fs/promises'

async function createVersionJson() {
	const versionJson = {
		timestamp: Date.now()
	}

	const filePath = join(__dirname, '../public/version.json');

	try {
		await writeFile(filePath, JSON.stringify(versionJson), {
			encoding: 'utf8',
			flag: 'w+' //  open the file for reading and writing, positioning the
			//             stream at the beginning of the file. The file is
			//             created if it does not exist.
		})

		console.log(`✅ Version timestamp is now ${versionJson.timestamp}`)
	} catch (error) {
		console.error('❌ Error creating file:\n', error)
	}
}

// call the function so that it can be executed as a node script
createVersionJson()

Check the source code here

Prebuild script

To store the version JSON before every build, we'll need a prebuild script. We'll also add a predev script so that the same notification also works in development.

// package.json

{
...
	"scripts": {
		...
		"predev": "node scripts/create-version-json.mjs",
		"dev": "next dev",

		"prebuild": "node scripts/create-version-json.mjs",
		"build": "next build",
		...
	}
...
}

ℹ️ You can check the npm documentation if you want to know more about pre/post scripts.

Check the whole package.json content here

Using SWR to cache the current version

Now let's create a custom hook to store the version timestamp into an immutable SWR hook:

// lib/usePageLoadVersion.ts

import useSWRImmutable from 'swr/immutable'

import versionJSON from '../public/version.json'

export default function usePageLoadVersion() {
	const { data, isLoading } = useSWRImmutable('pageLoadVersion', () => versionJSON.timestamp)

	return { pageLoadVersion: data, isLoading }
}

Check the source code here

With this hook we cache the current version from version.json on page load, or as soon as it runs.

Using SWR to watch for new versions

Now let's create another hook to get the latest version from version.json.

Here we'll fetch the latest version.json. When a there is a new version, the timestamp will be different than the immutable one. That way we know the app has a new version.

// lib/useLatestVersion.ts

import useSWR from 'swr'

export function useLatestVersion() {
	const { data, isLoading } = useSWR('latestVersion', fetchLatestVersion, {
		revalidateOnFocus: true,
		revalidateIfStale: true,
		revalidateOnReconnect: true,
		
		// we don't need to check on mount as the user has just entered the page
		revalidateOnMount: false
	})

	return { latestVersion: data, isLoading }
}

async function fetchLatestVersion() {
	const response = await fetch('/version.json', {
		headers: {
			'cache-control': 'no-cache'
		}
	})

	const version = await response.json()

	return version.timestamp as number
}

Check the source code here

Comparing the latest version with the cached one

Now that we have both versions, we can compare them to check whether the app has a new version. As a proof of concept, let's show a button to the user to reload the page whenever the versions differs. That way the user will get the latest and greatest version of your app.

// components/NotifyNewVersion.tsx

export default function NotifyNewVersion() {
	const { latestVersion, isLoading: isLoadingLatest } = useLatestVersion()
	const { pageLoadVersion, isLoading: isLoadingPageLoad } = usePageLoadVersion()

	const isLoading = isLoadingLatest || isLoadingPageLoad

	if (isLoading) {
		return <span>Loading...</span>
	}

	// latestVersion and pageLoadVersion will be undefined while loading,
	// so we check if both are truthy before comparing their values
	const hasNewVersion = Boolean(latestVersion && pageLoadVersion)
		? latestVersion !== pageLoadVersion
		: false	

	if(!hasNewVersion) {
	  return null
	}

	return (
		<>
			<span>
				The app has a new version!
			</span>

			<button 
				onClick={() => window.location.reload()} 
				type="button"
			>
				Click here to update
			</button>
		</>
	)
}

Check the source code here. It differs a bit from the code shown above so that the example app makes more sense, but the functionality is the same.

Cool, but where should I put it?

Now that we have the hooks and the component, it's just a matter of where do you want to notify your users.

You may want to notify only signed-in users if your app requires authentication, or only dynamic pages if you have certain pages that are fully static. In those cases you could put the component directly in the page:

// pages/your-page.tsx

import NotifyNewVersion from '../components/NotifyNewVersion'

export default function PageWhereUsersWillBeNotified() {
	return (
		<main>
			<NotifyNewVersion />
		</main>
	)
}

In case you want the component to be rendered in all pages, you can put it into the custom app:

// pages/_app.tsx

import type { AppProps } from 'next/app'

import NotifyNewVersion from '../components/NotifyNewVersion'

import '../styles/globals.css'

export default function App({ Component, pageProps }: AppProps) {
	return (
		<>
			<NotifyNewVersion />

			<Component {...pageProps} />
		</>
	)
}

You can also use the new Next.js layouts feature to share the component between multiple pages. For that to work we'll need to make a little change in the NotifyNewVersion component to indicate it's a client component:

'use client' // <-- just add this line

// ... the rest of the component stays the same as before:

export default function NotifyNewVersion() {
	const { latestVersion, isLoading: isLoadingLatest } = useLatestVersion()
	const { pageLoadVersion, isLoading: isLoadingPageLoad } = usePageLoadVersion()

	const isLoading = isLoadingLatest || isLoadingPageLoad

	if (isLoading) {
		return <span>Loading...</span>
	}

	// latestVersion and pageLoadVersion will be undefined while loading,
	// so we check if both are truthy before comparing their values
	const hasNewVersion = Boolean(latestVersion && pageLoadVersion)
		? latestVersion !== pageLoadVersion
		: false	

	if(!hasNewVersion) {
	  return null
	}

	return (
		<>
			<span>
				The app has a new version!
			</span>

			<button 
				onClick={() => window.location.reload()} 
				type="button"
			>
				Click here to update
			</button>
		</>
	)
}

Check the source code here

Then you can also use that component in your layout or any other server component that's under the app directory:

// app/app/nested/layout.tsx -- `/app/app` is duplicated in order to have `/app` in the route and easily indicate where you are while testing this

import type { ReactNode } from 'react'
import NotifyNewVersion from '../../../components/NotifyNewVersion'

export default function NestedRouteLayout({ children }: { children: ReactNode }) {
	return (
		<>
			{children}

			<NotifyNewVersion />
		</>
	)
}

Check the source code here

Abstracting hasNewVersion as a custom hook

In order to avoid having to always check for the versions and its loading states as we did in the NotifyNewVersion component, we can also abstract hasNewVersion as a reusable hook useHasNewVersion. It will also be useful if you need to check the app version in more than one place. SWR makes this easy:

// lib/useHasNewVersion.ts

import useSWR from 'swr'

import { useLatestVersion } from './useLatestVersion'
import { usePageLoadVersion } from './usePageLoadVersion'

export function useHasNewVersion() {
	const { latestVersion } = useLatestVersion()
	const { pageLoadVersion } = usePageLoadVersion()

	// wait until both versions are available
	const shouldCheck = Boolean(latestVersion && pageLoadVersion)

	const { data, isLoading } = useSWR(
		shouldCheck ? [latestVersion, pageLoadVersion] : null,
		() => latestVersion !== pageLoadVersion
	)

	return { 
		hasNewVersion: data, 
		isLoading: isLoading || shouldCheck === false 
	}
}

Check the source code here

Then the component can just use its value to know what to render:

// components/NotifyNewVersionWithHook.tsx

export default function NotifyNewVersionWithHook() {
	const { hasNewVersion, isLoading } = useHasNewVersion()

	if (!hasNewVersion)
		return null

	return (
		<>
			<span className={styles.title}>There is a new version!</span>

			<button 
				className={styles.button} 
				onClick={() => window.location.reload()}
			>
				Click here to update
			</button>
		</>
	)
}

Check the source code here

Conclusion

That's it! In this article we provided a way to notify users about new deployments with Next.js and SWR with a few hooks, a script and components to illustrate its usage. Now you can take it and use as you need as the code should be very easy to reuse in any Next.js project.

Don't forget that you can check the Notify users about new deployments with Next.js + SWR repository with a working app containing all the code presented in this article.