kt-tech.blog

画像

【実装】TanStack QueryでuseEffect/useStateパターンを撲滅する

Share
💡
データフェッチでよく書いてしまう useEffect + useState パターンを TanStack Query で置き換え、コードを簡潔にしつつキャッシュ・ローディング・エラーを自動管理する方法を紹介します。

はじめに

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]);
}
問題点: 手動のcancelledフラグ、isLoadingの管理、エラーハンドリング、キャッシュなし、依存配列のミスでバグ

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管理やキャンセルフラグは不要になります。

💡
queryClient.setQueryData でローカル更新する方法もありますが、チャットのような頻繁な追記ではローカルstateのほうがシンプルです。

まとめ

  • useEffect + useState のデータフェッチは useQuery に置き換えるだけで大幅に簡潔になる

  • enabled で条件付きフェッチ、staleTime でキャッシュ戦略を宣言的に記述

  • キャンセル・ローディング・エラーは TanStack Query が自動管理

  • Server Actionsも queryFn で直接呼べる