Closed2
SWRのページネーションと楽観的更新

想定
- ページネーション方式は、cursor-based pagination
- API は検索条件を渡すことができる
type PostID = string;
type Post = { id: PostID };
type Data = { posts: Post[]; nextCursor: PostID | null };
type Arguments = {
endpoint: "/api/posts";
cursor: string | null;
conditions: object[];
} | null;
const useInfinitePosts = ({ conditions }: { conditions: object[] }) => {
const keyLoader: SWRInfiniteKeyLoader<Data, Arguments> = useCallback(
(index, previousPageData) => {
if (previousPageData && previousPageData.nextCursor == null) {
// NOTE: これ以上データがない場合は null を返す
return null;
}
// NOTE: 1 ページ目の場合は previousPageData がない
if (index === 0 || previousPageData == null) {
return {
endpoint: "/api/posts",
cursor: null,
conditions,
} as const;
}
return {
endpoint: "/api/posts",
cursor: previousPageData.nextCursor,
conditions,
} as const;
},
[conditions]
);
const swrRes = useSWRInfinite<Data, Error, SWRInfiniteKeyLoader<Data, Arguments>>(
keyLoader,
async ({ endpoint, cursor, conditions }) => {
return apiClient.post(endpoint, {
cursor,
conditions,
});
},
{ keepPreviousData: true }
);
/**
* ページングの結果のうち、置き換えたいレコード1つを渡して楽観的更新する
*/
const mutateWithOptimisticUpdate = useCallback(
async (newPost: Post, waitPromise: Promise<unknown> = new Promise((r) => r(void 0))) => {
const generateNewData = (prev: Data[] | undefined): Data[] | undefined => {
if (!prev) {
return prev;
}
const next = [...prev];
const targetPageIndex = next.findIndex((page) =>
page.posts.some((n) => n.id === newPost.id)
);
if (targetPageIndex === -1) {
return prev;
}
const targetPage = next[targetPageIndex];
if (!targetPage) {
return prev;
}
const targetIndex = targetPage.posts.findIndex((n) => n.id === newPost.id);
if (targetIndex === -1) {
return prev;
}
const newPage = {
...targetPage,
posts: [
...targetPage.posts.slice(0, targetIndex),
newPost,
...targetPage.posts.slice(targetIndex + 1),
],
};
next[targetPageIndex] = newPage;
return next;
};
await swrRes.mutate(
async (data) => {
await waitPromise;
return generateNewData(data);
},
{
optimisticData: swrRes.data
? (prev) => generateNewData(prev) ?? [] // NOTE: optimisticData はコールバックの返り値として Data[] しか受け付けないので妥協 (swrRes.data に依存させることで、実質的に data が undefined になるケースを除外できている)
: undefined,
rollbackOnError: true,
}
);
},
[swrRes]
);
const loadNextPage = useCallback(() => swrRes.setSize((s) => s + 1), [swrRes]);
return Object.assign(swrRes, {
mutateWithOptimisticUpdate,
loadNextPage,
});
};

- ページ単位で楽観的更新できたら大量にページが読み込まれている場合でもパフォーマンスを担保できたなあと思う
-
https://swr.vercel.app/ja/docs/pagination#revalidate-specific-pages のとおり、再検証だけであれば特定のページに絞ることはできる
- 応用して、特定のページ以降のページ全てのように再検証することもできるはずなので、必要であれば適用したらいい
このスクラップは2024/09/10にクローズされました