

Reactでデータフェッチといえば useEffect + useState + isLoading + error… というボイラープレートが定番ですが、手動キャンセル・キャッシュ・再検証を自前で管理するとバグの温床になります。TanStack Query(旧React Query)を導入してこれらを一掃した実例を紹介します。
useEffect + useState → useQuery への置き換え手順
enabled オプションで条件付きフェッチを制御する方法
staleTime / Infinity の使い分け
Server Actions を queryFn で呼ぶパターン
// 典型的な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]);
}
// 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]);
}
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,
});
チャットのようにリアルタイムでメッセージを追記するケースでは、queryのdataをそのまま使えません(送信ごとにローカルでメッセージ追加するため)。queryのdataからローカルstateに初期値を同期するuseEffectは残りますが、手動のloading管理やキャンセルフラグは不要になります。
useEffect + useState のデータフェッチは useQuery に置き換えるだけで大幅に簡潔になる
enabled で条件付きフェッチ、staleTime でキャッシュ戦略を宣言的に記述
キャンセル・ローディング・エラーは TanStack Query が自動管理
Server Actionsも queryFn で直接呼べる