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)
- Next.js 公式ドキュメント
https://nextjs.org/docs - App Router(基本)
https://nextjs.org/docs/app - Route Handlers(API Routes)
https://nextjs.org/docs/app/building-your-application/routing/route-handlers
🟩 TanStack Query(React Query v5)
- TanStack Query 公式
https://tanstack.com/query/v5/docs/react/overview - useQuery
https://tanstack.com/query/v5/docs/react/reference/useQuery - キャッシュ(staleTime / gcTime)
https://tanstack.com/query/v5/docs/react/guides/caching - 再取得(refetch)
https://tanstack.com/query/v5/docs/react/guides/query-invalidation