2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Next.js × TanStack Query 入門

2
Posted at

useEffect との比較で理解するサーバー状態管理
Next.js で API を使った画面を作る際、次のようなコードを書いていませんか?

  • useState
  • useEffect
  • ローディング状態の管理
  • エラー処理
  • 再取得処理

毎回これを実装するのは大変です。

そこで便利なのが TanStack Query(旧 React Query) です。

この記事では、
「useEffect 版」と「TanStack Query 版」を並べて比較できるサンプルアプリを作りながら、
何が便利なのかを初心者向けに解説します。


TanStack Query とは

API など “サーバー側のデータ” を扱うためのライブラリです。

  • 自動キャッシュ
  • ローディング管理
  • エラー管理
  • 再取得(refetch)
  • ページ遷移後もキャッシュ保持
  • 無限スクロールなどの高度な機能

これらを簡単に扱えます。


採用実績と信頼性

  • npm 週 1,200 万ダウンロード以上
  • GitHub 星 4.7 万以上
  • React のサーバー状態管理の定番ライブラリ

商用サービスでもよく利用されています。


サンプルアプリを作る

プロジェクト作成

npx create-next-app@latest tanstack-sample --ts
cd tanstack-sample

ダミー API を作成

app/api/users/route.ts

import { NextResponse } from "next/server"

const USERS = [
  { id: 1, name: "佐藤", role: "フロントエンドエンジニア" },
  { id: 2, name: "鈴木", role: "バックエンドエンジニア" },
  { id: 3, name: "田中", role: "フルスタックエンジニア" },
]

export async function GET() {
  // ローディング比較用に少し遅らせる
  await new Promise((r) => setTimeout(r, 800))

  return NextResponse.json(USERS)
}

QueryClientProvider を設定

app/providers.tsx

"use client"

import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
import type { ReactNode } from "react"
import { useState } from "react"

export function AppProviders({ children }: { children: ReactNode }) {
  // QueryClient は 1 つだけ持ちたいので useState で保持
  const [queryClient] = useState(() => new QueryClient())

  return (
    <QueryClientProvider client={queryClient}>
      {children}
    </QueryClientProvider>
  )
}

app/layout.tsx

import type { Metadata } from "next"
import "./globals.css"
import { AppProviders } from "./providers"

export const metadata: Metadata = {
  title: "TanStack Query Sample",
  description: "Next.js と TanStack Query の比較デモ",
}

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="ja">
      <body>
        <AppProviders>{children}</AppProviders>
      </body>
    </html>
  )
}

useEffect 版 vs TanStack Query 版(比較ページ)

app/page.tsx

"use client"

import { useEffect, useState } from "react"
import { useQuery } from "@tanstack/react-query"

type User = { id: number; name: string; role: string }

// ----------------------------
// useEffect 版
// ----------------------------
function PlainUsersList() {
  const [users, setUsers] = useState<User[]>([])
  const [isLoading, setIsLoading] = useState(false)
  const [error, setError] = useState<string | null>(null)

  useEffect(() => {
    let cancelled = false

    async function fetchUsers() {
      try {
        setIsLoading(true)
        const res = await fetch("/api/users")
        if (!res.ok) throw new Error("failed")
        const data = await res.json()
        if (!cancelled) setUsers(data)
      } catch (e: any) {
        if (!cancelled) setError(e.message)
      } finally {
        if (!cancelled) setIsLoading(false)
      }
    }

    fetchUsers()
    return () => {
      cancelled = true
    }
  }, [])

  return (
    <section>
      <h2>useEffect 版</h2>
      {isLoading && <p>読み込み中…</p>}
      {error && <p>エラー: {error}</p>}
      <ul>
        {users.map((user) => (
          <li key={user.id}>{user.name}{user.role}</li>
        ))}
      </ul>
    </section>
  )
}

// ----------------------------
// TanStack Query 版
// ----------------------------
function QueryUsersList() {
  const { data, isLoading, isError, error, refetch } = useQuery<User[], Error>({
    queryKey: ["users"],
    queryFn: async () => {
      const res = await fetch("/api/users")
      if (!res.ok) throw new Error("failed")
      return res.json()
    },
    staleTime: 1000 * 60, // 1分キャッシュ
  })

  return (
    <section>
      <h2>TanStack Query 版</h2>

      <button onClick={() => refetch()}>再読み込み</button>

      {isLoading && <p>読み込み中…</p>}
      {isError && <p>エラー: {error?.message}</p>}
      <ul>
        {(data ?? []).map((user) => (
          <li key={user.id}>{user.name}{user.role}</li>
        ))}
      </ul>
    </section>
  )
}

// ----------------------------
// ページ本体
// ----------------------------
export default function Page() {
  return (
    <main>
      <h1>Next.js × TanStack Query 比較サンプル</h1>

      <div style={{ display: "grid", gap: "24px", gridTemplateColumns: "1fr 1fr" }}>
        <PlainUsersList />
        <QueryUsersList />
      </div>
    </main>
  )
}

比較してわかること

1. ローディングとエラー処理が標準で揃っている

useEffect 版

  • ローディング用 state を自分で用意する必要がある
  • エラー用 state も自分で管理する必要がある
  • try / catch / finally の記述が増える

TanStack Query 版

  • isLoading
  • isError
  • error
    これらが最初から返ってくるため、状態管理が非常にシンプル。

2. 再取得(refetch)が圧倒的に簡単

useEffect 版

  • fetch 関数を自作し、手動で呼び出す必要がある
  • 再取得時もローディング管理を記述する必要がある

TanStack Query 版

  • refetch() を呼ぶだけで再取得が完了する
  • 再取得時のローディング・エラー処理も自動

3. キャッシュが強力で UX が向上する

staleTime の指定だけで、以下が自動で行われる。

  • ページ遷移後もデータを即時表示
  • 無駄な API リクエストを削減
  • スムーズで快適な操作感が得られる

4. コード量が減り、読みやすくなる

useEffect 版

  • データ取得処理
  • ローディング管理
  • エラー管理
  • 再取得のロジック
    これらがコンポーネント内にまとまり、肥大化しやすい。

TanStack Query 版

  • useQuery にデータ取得ロジックを任せられる
  • UI とロジックが分離され、コンポーネントがスッキリする
  • 中規模以上のアプリほど恩恵が大きい

まとめ

  • TanStack Query はサーバー状態管理の決定版
  • useEffect で書くよりコードが圧倒的に簡潔になる
  • ローディング、エラー、キャッシュ、再取得が自動
  • Next.js と組み合わせると生産性が大きく上がる

📚 関連ドキュメント

🟦 Next.js(App Router)


🟩 TanStack Query(React Query v5)


📦 このサンプルの GitHub

🔗 https://github.com/Kazuya-Sakashita/tanstack-query-sample

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?