nino
  • Docs
  • Registry
  • Membership

Command Palette

Search for a command to run...

スポンサー
メニュー

SWR

 

8m 40s

SWR はクライアントからデータを取得する際に使用するライブラリです。Tanstack Query と同じカテゴリのツールになります。

ライブデモ

いつ使うのか?

以下のシーンで使用します。

  • 無限スクロールなど、データを積み上げていくような場合
  • 複雑な構造における楽観的更新が必要な場合
  • シンプルなグローバル状態の管理
  • Cache Component が動かない環境にデプロイする場合

以前までパーソラナイズされたデータの取得を SWR で行うことで、画面を静的生成できるメリットがありました。今では Next.js の Cache Component を有効にすることで PPR(部分的な事前生成)が可能になったため、静的生成のために SWR を使う必要はなくなりました。

使い方

  1. SWR からアクセスするエンドポイントを作成
  2. SWR のカスタムフックを作成
  3. カスタムフックを使用してデータを取得

Server Actions をデータ取得用途で使うのは非推奨であるため、SWRからデータを取得するためにルートハンドラー(APIエンドポイント)を作成します。

基本的な仕組み

  • SWR にはキーと取得関数(通常共通)を設定します。同じキーを参照する場合はキャッシュが利用されます。
  • キーが変わると取得関数が再実行されます。
  • 通常SWRキーごとにカスタムフックを作成し、再利用します。
  • mutate によりデータを再取得し、キャッシュを更新します。
  • SWR からはさまざまな状態やメソッドが返却されます
    • data: データ
    • error: エラー
    • isLoading: ローディング中かどうか(初回)
    • isValidating: データが検証中かどうか(初回以降)
    • mutate: データを再取得する

ベーシックなSWRフックです。

Loading...

Immutable とは

swr/immutable を使うとキャッシュが自動で更新されなくなります。無駄なデータの再取得を防ぐことができます。

Loading...

基本機能は同じですが、mutate を実行しない限り一度キャッシュした情報がずっと使用されます。データ通信を節約できる反面、mutate を手動で行わないと古いデータがユーザーに表示され続けるので注意が必要です。

immutable を使わない場合、以下のタイミングでデータが自動的に再取得されます。

  • フォーカスが外れたとき
  • 使用するコンポーネントがマウントされた時
  • ブラウザのタブを切り替えたとき

たとえば画面AでSWRフックを使用していたとします。画面Bに移動すると画面Aはアンマウントされ、再び戻ってくると画面Aが再度マウントされるため、mutate(データの再取得&キャッシュの更新)が実行されます。つまり画面を往復するたびにデータ取得が発生します。

無限スクロールにSWRを使う

エンドポイントの作成

エンドポイントでは内部的にデータアクセスレイヤー(データ取得用の関数)を使用します。これによりセキュリティリスクを低減します。

Loading...
Loading...

カスタムフックで共有するフェッチャーを作成

fetch の処理は基本固定なので共有化しておくと便利です。

Loading...

カスタムフックの作成

Loading...

画面の実装

追加読み込みの判定に react-intersection-observer を使用します。

Loading...
Loading...

複雑な構造における楽観的更新

シンプルな構造の場合 useOptimistic で楽観的更新が可能ですが、複雑な構造の場合は useSWR で使う方がシンプルです。

SWRフックを作成

Loading...

mutate の使用時に楽観的更新を有効にする

Loading...

これにより、新規タスクの作成と同時に画面が更新されますが、サーバー側の処理が失敗したら元に戻ります。多くの場合送信用のコンポーネントと一覧表示用のコンポーネントは別の場所になるため、useOptimistic を使うと Context を介して楽観的更新を伝搬させることになります。SWRはコンポーネントの階層を問わないため、楽観的更新の反映を自動で行うことができます。

シンプルな状態管理用途で使う

フェッチャーを空にすることでシンプルなグローバル状態管理用途で使用できます。

Loading...
Loading...
Discord で質問する
何か気になったこと、分からないことがあれば気軽に質問してください!
DiscordDiscordで質問する
nino

Developer

XXGitHubGitHubYouTubeYouTubeZennZennDiscordDiscord

    Links

  • Documentation
  • Registry
  • Architecture

    Tools

  • Feed
  • Status

    Policies

  • Terms of Service
  • Privacy Policy
  • Legal
Documentation
Getting Started
  • Changelog
Guides
  • Webアプリの環境構築
  • ニュース収集&AI要約
  • Turso のテーブル移行
  • 日時の管理
  • 中規模のリストをブラウザにキャッシュし、クライアントでフィルタする
  • Search Params Dialog
  • コードの整理
  • AGENTS.md
  • Better Auth
  • プロダクト開発ポリシー
  • 推奨ツール
  • Proxy.ts
  • 多言語対応
  • SWR
  • Cache Component
  • Next.js の課題、不具合
  • アプリ開発フロー
  • プロンプトガイド
  • CSS Tips
  • Font Family
  • セルフブランディング
  • セルフオンボーディング
  • オフィスツール
  • Email
  • Breadcrumb(パンくず)
  • 並べ替え
import { useSWR } from "swr";
import { fetcher } from "@/lib/utils";

export function useTasks() {
  const { data, error, isLoading, isValidating, mutate } = useSWR("/api/tasks", fetcher);
  return { tasks: data, error, isLoading, isValidating, mutate };
}
const swr from 'swr';
const swrImmutable from 'swr/immutable'; // immutable バージョン
import "server-only";
import { db, tasks } from "@workspace/db";
import { and, asc, gt } from "drizzle-orm";

export const getTasks = async (params?: {
  cursor?: number;
  limit?: number;
}) => {
  // 必要に応じて権限検証

  const limit = params?.limit ?? 10;
  const cursor = params?.cursor;

  const tasksData = await db.query.tasks.findMany({
    limit: limit + 1, // hasMore判定のため+1
    ...(cursor && {
      where: gt(tasks.id, cursor),
    }),
    orderBy: asc(tasks.id),
  });

  const hasMore = tasksData.length > limit;
  const items = hasMore ? tasksData.slice(0, -1) : tasksData;
  const nextCursor = hasMore ? items[items.length - 1]?.id : undefined;

  return {
    items,
    hasMore,
    nextCursor,
  };
};
import { getTasks } from "@/data/task";
import { NextRequest, NextResponse } from "next/server";

export async function GET(request: NextRequest) {
  const searchParams = request.nextUrl.searchParams;
  const cursor = searchParams.get("cursor")
    ? Number(searchParams.get("cursor"))
    : undefined;
  const limit = searchParams.get("limit")
    ? Number(searchParams.get("limit"))
    : undefined;

  const result = await getTasks({ cursor, limit });
  return NextResponse.json(result);
}
export const fetcher = async (url: string) => {
  const response = await fetch(url);
  return response.json();
};
import useSWRInfinite from "swr/infinite";

// SWR用のキー生成関数
const getKey = (
  pageIndex: number,
  previousPageData: Task[] | null,
  pageSize: number
) => {
  if (previousPageData && !previousPageData.hasMore) return null;

  const cursor = pageIndex === 0 ? undefined : previousPageData?.nextCursor;
  return ["tasks-infinite-scroll", cursor, pageSize];
};

// APIフェッチャー
const fetcher = async (
  key: [string, number | undefined, number]
): Promise<Task[]> => {
  const cursor = key[1];
  const pageSize = key[2];

  const params = new URLSearchParams({
    limit: pageSize.toString(),
  });

  if (cursor !== undefined) {
    params.append("cursor", cursor.toString());
  }

  const response = await fetch(`/api/tasks?${params}`);

  if (!response.ok) {
    throw new Error("Failed to fetch tasks");
  }

  return response.json();
};

export function useInfiniteTasks() {
  const pageSize = 5;
  const { data, error, isLoading, isValidating, size, setSize, mutate } =
    useSWRInfinite(
      (pageIndex, previousPageData) =>
        getKey(pageIndex, previousPageData, pageSize),
      fetcher,
      {
        revalidateFirstPage: false,
        revalidateOnFocus: false,
      }
    );

  const items = data ? data.flatMap((page) => page) : [];
  const hasMore = data ? data[data.length - 1]?.hasMore : true;

  const loadMore = () => {
    if (hasMore && !isLoading) {
      setSize(size + 1);
    }
  };

  return {
    items,
    isLoading,
    hasMore,
    error,
    loadMore,
    isValidating,
    mutate,
  };
}
pnpm add react-intersection-observer
import { useInfiniteTasks } from "@/swr/user-infinite-tasks";
import { InView } from "react-intersection-observer";

export default function TasksPage() {
  const { items, isLoading, hasMore, error, loadMore, isValidating } =
    useInfiniteTasks();

  return (
    <div>
      <h1>Tasks</h1>

      {items.map((item, index) => {
        const isLast = index === items.length - 1;
        return (
          <div key={item.id}>
            <p>{item.title}</p>

            {isLast && hasMore && (
              <InView
                as="div"
                onChange={(inView) => {
                  if (inView && !isValidating) {
                    loadMore();
                  }
                }}
                threshold={0.1}
              >
                <div className="h-4" />
              </InView>
            )}
          </div>
        );
        })}

      {isValidating && <div>Loading...</div>}

      {!hasMore && items.length === 0 && <div>すべてのタスクを読み込みました</div>}
    </div>
  );
}
import { useSWR } from "swr";
import { fetcher } from "@/lib/utils";

export function useTasks() {
  const { data, error, isLoading, isValidating, mutate } = useSWR("/api/tasks", fetcher);
  return { tasks: data, error, isLoading, isValidating, mutate };
}
import { useTasks } from "@/swr/use-tasks";

export default function TasksPage() {
  const { tasks, isLoading, isValidating, mutate } = useTasks();

  const handleCreateTask = async (newTask: NewTask) => {
    mutate(
      async () => {
        await createTask(newTask);
        return undefined;
      },
      {
        optimisticData(currentData) {
          return [...(currentData || []), newTask];
        },
        populateCache: false,
      }
    )
      .then(() => {
        toast.success("タスクを作成しました");
      })
      .catch(() => {
        toast.error("タスクを作成できませんでした");
      });
  };

  return <div>{tasks?.map((task) => <p key={task.id}>{task.title}</p>)}</div>;
}
import { useSWR } from "swr/immutable";

export function useColor() {
  const { data, mutate } = useSWR("/api/color", null, {
    fallbackData: "red", // 初期値
  });
  return { color: data, setColor:mutate };
}
import { useColor } from "@/swr/color";

export default function TasksPage() {
  const { color, setColor } = useColor();
  return <div>
    <p>{color}</p>
    <button onClick={() => setColor("blue")}>Change Color</button>
  </div>;
}