React Query を使っていて気になった SWR とのいくつかの違い

2022.03.14

MAD 事業部の高橋ゆうきです。

REST API である場合、プライベートでは SWR を使うことが多いのですが、昨年から案件では React Query を使っているのでいくつかの違いを感じることがありました。ここでは React Query のドキュメントにある表 から気になった違いをいくつかピックアップしてみます。

比較

Query Key Change Detection

  • React Query - Deep Compare (Stable Serialization)
  • SWR - Shallow Compare Deep Compare (Stable Serialization)

2021 年 2 月 20 日現在上記のように記載されていますが、SWR も 1.1.0 以降は ドキュメントにも記載されているように、stable serialization となっています。

Query Data Change Detection / Query Data Memoization Level

React Query では Structural Sharing をしていますが、SWR はデフォルトで dequal が使用されています。

Lagged Query Data

これは React Query の公式のドキュメントでは次のように説明されています。

React Query provides a way to continue to see an existing query's data while the next query loads (similar to the same UX that suspense will soon provide natively).

実際に Pager のようなものを実装するとわかるのですが、例えば以下のような型のレスポンスがあったと仮定してみます。

export type ListContentsResponse<T> = {
  contents: T[];
  totalCount: number;
  offset: number;
  limit: number;
};

これで初期の 1 ページ目から 2 ページ目を表示するとき、2 ページ目のキャッシュが存在しない場合には何も表示できるものがないのでローディングを表示することになります。このとき、キャッシュが存在しないため先程まで表示されていた Pagination までローディングに巻き込まれて画面から消えてしまいます。可能であればローディング中に 1 ページ目で表示させた状態を画面に残しておきたいはずです。

上記を設定 1 つで可能にするのが、keepPreviousData になります。詳細については公式ページにあるドキュメントの Paginated / Lagged Queries にあります。

Partial Query Matching

部分的なクエリマッチングをサポートする API であれば、Query Filters を使って、自由度の高いクエリの無効化を実現できます。

queryClient.invalidateQueries("posts");

// 以下のクエリを無効化する
const allPosts = useQuery("posts", fetchPosts);
const firstPage = useQuery(["posts", { page: 1 }], fetchPosts);

// allPosts のみを無効化したい場合
queryClient.invalidateQueries("posts", { exact: true });

key の管理をうまく設計できないと有効活用が難しく、活用しすぎてただ無意味にキャッシュの制御が複雑になるだけということもあるので、使えるときに便利に使うくらいの感じで使うことが多いです。

Stale Time Configuration

staleTime については基本的なことを Important Defaults で確認後に Caching Examples を見ると理解しやすいです。

デフォルトでは staleTime が 0 になっています。負荷の高い API がある場合、ちょっとしたページの移動によって都度バックグラウンドで fetch するわけにもいかないので設定の変更が必要になります。

staleTime を 0 にして、あまりよくない記述方法は以下のようなもの。

import { fetcher } from "./api";

export const Parent = (): JSX.Element => {
  const { data, isLoading } = useQuery("/users", fetcher);

  if (isLoading) {
    return <div>isLoading</div>;
  }

  return <Child />;
};

export const Child = (): JSX.Element => {
  const { data, isLoading } = useQuery("/users", fetcher);

  if (isLoading) {
    return <div>isLoading</div>;
  }

  return <>{data && <div>{data.name}</div>}</>;
};

これでは初回のローディングで二度 fetch してしまいます。ローディング完了後に Child がマウントされ新しいインスタンスが画面に表示されるためです。

Suspense

どちらも experimental でサスペンスモードを有効化することで利用可能になります。

サスペンスモード時に並列で複数の異なるエンドポイントを叩きたいとき、React Query では useQueries で回避できます

SWR では一時的な対応として preload することで回避する方法が Issue に記載されています。

さいごに

はじめにプライベートでは SWR を使うことが多いと書きましたが、最近は React Query を使っていて SWR を使うことが減ってきています。仕事で使っているので React Query に慣れてきたこともあるのですが、単純に機能の豊富さで嬉しいことが多いからです。

明確に採用の理由が説明できる場合にのみ SWR にして、もしどちらを採用していいか迷っているような場合には、React Query を使っておくと良さそうだと感じています。