Prompting Users to Reload Your Next.js App After an Update
Tags: nextjs, react, javascript
Published: 2021-04-05 16:37:25

The pages of a [Next.js](https://nextjs.org/) application are served in one of two ways: server-side rendering, or client-side rendering. It's important to understand the distinction, and when each scenario occurs. (There is also [static generation](https://nextjs.org/docs/advanced-features/automatic-static-optimization), but we will disregard that for this walkthrough.)

Server-side rendering is when the underlying Node.js server is handling the request, loading the corresponding page component (and any data dependencies), and returning the populated HTML that results. A page will be server-side rendered if it's the initial request to load the page, and the page implements either [`getInitialProps`](https://nextjs.org/docs/api-reference/data-fetching/getInitialProps) or [`getServerSideProps`](https://nextjs.org/docs/basic-features/data-fetching#getserversideprops-server-side-rendering).

Client-side rendering is when the Javascript in the browser has taken over the handling of the request, and React will handle rendering the new page component and reconciling any differences. Client-side rendering occurs when the user has already loaded your application, and is navigating via the Next.js router (either [directly](https://nextjs.org/docs/api-reference/next/router#userouter)) or via the [`<Link />`](https://nextjs.org/docs/api-reference/next/link) component.

The important  caveat with client-side rendering is that once the user has loaded the application and each of the pages, requests are no longer being made to the server to render any of them -- the client is handling it all. This means that if you deploy a new version of your application while someone is using it, they could continue seeing and using the previous version of your application until they happen to reload.

This can cause issues if you're making breaking changes, or fixing bugs, or making any other changes you'd prefer your users see ASAP. This risk is multiplied by the number of people using your application. So how can you handle new deploys on the client to ensure our users get the latest version?

Next.js allows [customizing the Webpack configuration](https://nextjs.org/docs/api-reference/next.config.js/custom-webpack-config) used at build time via a `next.config.js` file. It will automatically pass in various relevant arguments; the one that we're interested in is `buildId`. By default, this is a random string unique to each build.

Combined with Webpack's [`DefinePlugin`](https://webpack.js.org/plugins/define-plugin/), you can expose this buildId to our application by replacing any checks for `process.env.BUILD_ID` with the real `buildId`:

```ts
// next.config.js

module.exports = {
  ...
  webpack(config, { buildId }) {
    config.plugins.push(
      new webpack.DefinePlugin({
        'process.env': {
          BUILD_ID: JSON.stringify(buildId),
        },
      }),
    );

    return config;
  },
};
```

This means that the resulting bundles served on the client will have the real `buildId` available to them when checking `process.env.BUILD_ID`. Since these bundles stay loaded as client-side navigation occurs, this will remain a static reference to the `buildId` loaded on the client.

Next, you'll want to also expose this `process.env.BUILD_ID` variable in our server-side environment. This is because when you deploy a new version of your application, anything being handled by the server will immediately be operating on the newest version. You can do this via Next.js's [API routes](https://nextjs.org/docs/api-routes/introduction):

```ts
// pages/api/build-id.ts

import { NextApiRequest, NextApiResponse } from 'next';

export default (_req: NextApiRequest, res: NextApiResponse): void => {
  res.status(200).json({
    buildId: process.env.BUILD_ID,
  });
};
```

With this new endpoint exposing `process.env.BUILD_ID` from the server, you have a route we can hit at any time to get the *newest* deployed buildId: `/api/build-id`.

Since the client will now have a static reference to its own `buildId`, and the server now has the endpoint always returning the newest `buildId`, we can implement our own polling and diffing to determine if the user needs to reload. Below is a component that encapsulates this logic, polling for the latest `buildId` every 30 seconds via a `useInterval` hook. This can then be rendered anywhere in your application.

```ts
import { useEffect, useRef } from 'react';
import request from 'superagent';

function useInterval<P extends Function>(
  callback: P,
  { interval, lead }: { interval: number; lead?: boolean },
): void {
  const savedCallback = useRef<P>(null);

  useEffect(() => {
    savedCallback.current = callback;
  }, [callback]);

  useEffect(() => {
    const tick = (): void => savedCallback.current();

    lead && tick();

    if (interval !== null) {
      const id = setInterval(tick, interval);

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

export function DeployRefreshManager(): JSX.Element {
  useInterval(
    async () => {
      const { buildId } = (await request.get('/api/build-id')).body;

      if (buildId && process.env.BUILD_ID && buildId !== process.env.BUILD_ID) {
        // There's a new version deployed that we need to load
      }
    },
    { interval: 30000 },
  );

  return null;
}
```

At walrus.ai, we display a non-closable modal to the user with just one possible action: reloading the page. Clicking this button simply  calls `window.location.reload()`.

![Screenshot of walrus.ai's new version modal](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gaaf3cemr5j748frb42x.png)

This is achieved by simply setting a boolean state value in the above if statement, and conditionally returning your modal element from the component instead of always returning null.

You'll now be able to rely on the fact that your users are always using the latest version of your application, or at least being prevented from taking actions until they've reloaded.