January 20, 2021

Updating Static Next.js Pages Instantly

Lightning-fast pages, instantly updated

Update: 14th September 2021

Amended to reflect the newer SWR 1.0 API.

For the last few months, I've been working full-time on Give&Bake; allowing users to share their favourite recipes with their favourite people. Recipes that need to be fast, secure and always online. With static generation built-in, Next.js was the tool of choice.

Incremental Static Generation

Incremental Static Generation (ISR) is one of my favourite features of Next.js; providing all the benefits of static pages, with the ability to update pages in the background as traffic comes in.

On paper it sounds like ISR would work perfectly for Give&Bake's use-case, but there is an important caveat to consider. When a user visits a page with ISR enabled, an update will be triggered in the background for the next (it's in the name) user, not the current user.

If a user edited their recipe on Give&Bake, they wouldn't see their changes on the static recipe page until they hit refresh; a far from ideal user experience.

A Sprinkle of SWR

SWR ("stale-while-revalidate") is another great little library from the folks at Vercel, commonly used to fetch and revalidate data on the client-side.

By disabling revalidateOnMount, we can skip the initial fetch altogether and use fallbackData pre-fetched via ISR (through getStaticProps); with a unique cache key of our post for later use.

You can name your key however you wish. In this case, the key matches an /api route for consistency's sake; e.g. for other pages that might require SWR's client-side fetching.

// pages/post/[id]
import useSWR from "swr";
import { fetcher } from "@/utils/fetcher";

export const getStaticPaths = async () => {
  // …custom logic to create paths for each `id`
};

export const getStaticProps = async () => {
  // …custom logic to populate `id` and `fallbackData`
};

const PostPage = ({ id, fallbackData }) => {
  // useSWR will:
  // 1. Create a cache key for the post
  // 2. Use the `fallbackData` **without** triggering a fetch on mount.
  const { data, error } = useSWR("/api/post" + id, fetcher, {
    fallbackData,
    revalidateOnMount: false,
  });

  const post = !error && data?.post;

  return (
    <article>
      <h1>{post.title}</h1>
      <p>{post.content}</p>
    </article>
  );
};

export default PostPage;

On the "edit" page, we can mutate the post's unique cache key with any updated data. On redirect back to the post page, the user will see their updated post instantly, with ISR triggered in the background for the next user.

// pages/post/edit/[id]
import { useRouter } from "next/router";
import useSWR from "swr";
import { fetcher } from "@/utils/fetcher";

export const getStaticPaths = async () => {
  // …custom logic to create paths for each `id`
};

export const getStaticProps = async () => {
  // …custom logic to populate `id` and `fallbackData`
};

const PostEditPage = ({ id, fallbackData }) => {
  const router = useRouter();
  const { mutate } = useSWR("/api/post" + id, fetcher, {
    fallbackData,
    revalidateOnMount: false,
  });

  const handleSubmit = (newData) => {
    // mutate the post however you wish, in this case prepend to other posts.
    mutate(
      "/api/post" + id,
      (prevData) => ({
        post: {
          ...newData,
          ...prevData,
        },
      }),
      // Disable revalidation
      false
    );

    // Prevent the user from navigating `back` to this page
    router.replace(`/post/${id}`);
  };

  return <form>{/* Form Logic */}</form>;
};

export default PostEditPage;

When used together, ISR and SWR offer lightning-fast static pages, instantly updated for the current user and statically regenerated for the next.

Visit the demo to see this in action…

…alternatively, join the Give&Bake beta waitlist to try the recipe pages first-hand.

––– views