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:
profile
data appear in/
- the data is updated in
/profile/edit
Then do the following actions:
- Visit
/
- Go to
/profile/edit
and update the data - 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.
- Update cache manually
- 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>
);
}