useSWR cache management

by mmyoji

3 min read

Next.js + useSWR is awesome and gives you a great experience for frontend dev.

But they also sometimes cause trouble, and I explain one of them in this post.

If your app has a very simple components pyramid, fetch data in getServerSideProps and pass them as props to components without useSWR.

After the app gets more complicated and you'd like to manage both data and component in a single Function Component with using useSWR, you should care about useSWR cache management for better UX.

Deps

  • next@11.x
  • swr@1.0.x

Example

Let's think it this in an exmaple:

  1. profile data appear in /
  2. the data is updated in /profile/edit

Then do the following actions:

  1. Visit /
  2. Go to /profile/edit and update the data
  3. Go back to /

The page is rendered with older data first (for a slight few moment), then useSWR updates the page with newer data.

Here's the code example:

// components/ProfileSidebar.tsx
export function ProfileSidebar() {
  const { data } = useSWR("/profile");

  if (!data) return <Loading />;

  return <div>Name: {data.name}</div>;
}

// pages/index.tsx
import { ProfileSidebar } from "../components/ProfileSidebar";

export default function IndexPage() {
  return (
    <>
      <SomeComponent />
      <ProfileSidebar />
    </>
  );
}

// pages/profile/edit.tsx
interface FormContent {
  name: string;
  // ...
}

export default function EditProfilePage() {
  const [data, setData] = useState<FormContent>({ name: "", ... });

  const handleSubmit: FormEventHandler<HTMLFormElement> = async (event) => {
    event.preventDefault();

    await updateProfile(data);
  };

  return (
    <div>
      <form onSubmit={handleSubmit}>{/** `profile` is editable here */}</form>
    </div>
  );
}

I personally think this is okay.

But if your client doesn't allow this behaviour and says "Why the older data appears in a moment? It's ugly."

Then you have to update the cache data before visiting the / page.

Solutions

There're 2 approaches for this.

  1. Update cache manually
  2. Delete cache

Update cache manually

+import { useSWRConfig } from "swr";

export default function EditProfilePage() {
  const [data, setData] = useState<FormContent>({ name: "", ... });
+ const { mutate } = useSWRConfig();

  const handleSubmit: FormEventHandler<HTMLFormElement> = async (event) => {
    event.preventDefault();

-   await updateProfile(data);
+   const newProfile = await updateProfile(data);
+   mutate("/profile", newProfile, false);
  };

If the API endpoint doesn't return the exepected data, you can't take this manual mutation apporach. Then in most cases, the next deleting cache approach would be simpler and easier.

Delete cache

+import { useSWRConfig } from "swr";

export default function EditProfilePage() {
  const [data, setData] = useState<FormContent>({ name: "", ... });
+ const { cache } = useSWRConfig();

  const handleSubmit: FormEventHandler<HTMLFormElement> = async (event) => {
    event.preventDefault();

    await updateProfile(data);
+   cache.delete("/profile");
  };

Delete cache by the key and force useSWR to re-fetch data when visiting the page. Due to this, users will see loading state again in the / page, of course.

If you know better solutions, please tell me on Twitter.

fallbackData option

This is a bit different story.

If you pass fallbackData to useSWR, it is used only when cache data is not found.

// pages/posts/index.tsx
interface Props {
  posts: Post[];
}

export const getServerSideProps: GetServerSideProps<Props> = async () => {
  const posts = await fetchPosts();

  return {
    props: {
      posts,
    },
  };
};

export default function PostsPage({
  posts,
}: InferGetServerSidePropsType<typeof getServerSideProps>) {
  // 1. `data` is the same with `posts` when you visit this page first time. (with no useSWR cache)
  // 2. After you visit the different page (e.g., through `next/link`) and come back to this page,
  //    `getServerSideProps` is called and new `posts` is passed, but the `fallbackData` is not used.
  const { data } = useSWR("/api/posts", fetcher, { fallbackData: posts });

  return (
    <ul>
      {data!.map((post) => <Post key={post.id} {...post} />)}
    </ul>
  );
}