
【実装】TanStack QueryでuseEffect/useStateパターンを撲滅する
はじめに
Reactでデータフェッチといえば useEffect + useState + isLoading + error… というボイラープレートが定番ですが、手動キャンセル・キャッシュ・再検証を自前で管理するとバグの温床になります。TanStack Query(旧React Query)を導入してこれらを一掃した実例を紹介します。
この記事でわかること
-
useEffect + useState → useQuery への置き換え手順
-
enabled オプションで条件付きフェッチを制御する方法
-
staleTime / Infinity の使い分け
-
Server Actions を queryFn で呼ぶパターン
Before: useEffect + useState
// 典型的なuseEffect/useStateパターン(問題だらけ)
function useChat() {
const [messages, setMessages] = useState([]);
const [isSessionLoading, setIsSessionLoading] = useState(true);
useEffect(() => {
if (!userId) return;
let cancelled = false; // 手動キャンセルフラグ
async function loadSession() {
setIsSessionLoading(true);
try {
const character = await getCharacterBySlug(slug);
if (!character || cancelled) return;
const session = await getOrCreateTodaySession(userId, character.id);
if (cancelled) return;
setMessages(session.messages);
} catch (error) {
console.error(error); // エラーハンドリング手動
} finally {
if (!cancelled) setIsSessionLoading(false);
}
}
loadSession();
return () => { cancelled = true; }; // クリーンアップ手動
}, [userId, slug]);
}
After: useQuery
// queryFnにロジックを切り出し
async function fetchTodaySession(userId: string, slug: string) {
const character =
(await getCharacterBySlug(slug)) ?? (await getCharacterBySlug("akari"));
if (!character) return null;
const session = await getOrCreateTodaySession(userId, character.id);
return {
id: session.id,
lastResponseId: session.lastResponseId,
messages: session.messages.map((m) => ({
id: m.id,
role: m.role,
content: m.content,
timestamp: new Date(m.createdAt),
})),
};
}
function useChat() {
const { data: chatSession, isLoading: isSessionLoading } = useQuery({
queryKey: ["chatSession", userId, selectedSlug],
queryFn: () => fetchTodaySession(userId!, selectedSlug),
enabled: !!userId, // userIdがないときはフェッチしない
staleTime: Infinity, // 手動でinvalidateするまでキャッシュ有効
});
// queryのdataが変わったらローカルstateに同期
useEffect(() => {
if (!chatSession) return;
sessionIdRef.current = chatSession.id;
setMessages(chatSession.messages);
}, [chatSession]);
}
キーポイント: enabled オプション
useSessionが非同期でuserIdがundefinedの間はフェッチを止めたい場合、enabled: !!userId とするだけで制御できます。useEffectの if (!userId) return; が不要になります。
// キャラ一覧取得もenabledで制御
const { data: characters = FALLBACK } = useQuery({
queryKey: ["characters", session?.user?.id],
queryFn: () => getCharacters(session?.user?.id),
staleTime: 5 * 60 * 1000, // 5分キャッシュ
enabled: !!session?.user?.id,
});
useEffectを完全に0にできないケース
チャットのようにリアルタイムでメッセージを追記するケースでは、queryのdataをそのまま使えません(送信ごとにローカルでメッセージ追加するため)。queryのdataからローカルstateに初期値を同期するuseEffectは残りますが、手動のloading管理やキャンセルフラグは不要になります。
まとめ
-
useEffect + useState のデータフェッチは useQuery に置き換えるだけで大幅に簡潔になる
-
enabled で条件付きフェッチ、staleTime でキャッシュ戦略を宣言的に記述
-
キャンセル・ローディング・エラーは TanStack Query が自動管理
-
Server Actionsも queryFn で直接呼べる
最新記事
- 【設定・環境構築】OpenNext でNext.js SSGサイトをCloudflare Workersにデプロイする完全ガイド
2026/3/19
- 【実装】Notion calloutブロックをNext.jsでカラフルなUIコンポーネントとして表示する
2026/3/19
- 【トラブルシューティング】Cloudflare Pages → Workers 移行で遭遇したEdge Runtime問題集
2026/3/19
- 【実践】Next.js 13→16メジャーアップグレードの全記録 — 破壊的変更と対応策
2026/3/19
- 【自動化】Gemini Imagen APIでブログのeyecatch画像を自動生成してR2にアップロードする
2026/3/19
- 【実装】Notion APIでブログシステムを構築する(Next.js 13 App Router × SDK v5)
2026/3/19
- 【移行ガイド】microCMSからNotion APIへブログCMSを完全移行する
2026/3/19
- 【トラブルシューティング】本番デプロイで遭遇した問題と解決策まとめ
2026/3/15
- 【環境構築】Next.js × Cloudflare Workers の本番環境を一から構築する
2026/3/15
- 【設定・環境構築】Neon → Prisma Postgres 移行とローカル開発環境の構築
2026/2/26


