🐎

TanStack Queryで条件付きフェッチ

2024/10/15に公開

ユーザーの入力に応じてデータを取得したいときなど、条件付きで useQueryuseSuspenseQuery を使いたい場面があります。この記事ではディスカッション上で紹介されていた方法をまとめます。

useSuspenseQueryを使う場合

useSuspenseQuery を使う場合は2通りの方法があります。

コンポーネントを分割する

Suspenseではデータ取得が成功していることが期待されるため、条件によるレンダリングやエラーハンドリングは親コンポーネントで行います。

TodoList.tsx
"use client";

import { useSuspenseQuery } from "@tanstack/react-query";
import { Suspense, useState } from "react";

type Todo = {
  id: number;
  userId: number;
  title: string;
  completed: boolean;
};

// データ取得用の関数
const getTodos = async () => {
  const res = await fetch("https://jsonplaceholder.typicode.com/todos");
  const todos = (await res.json()) as Todo[];

  return todos;
};

/** 条件によるレンダリングを担う親コンポーネント **/
const TodoList = () => {
  const [isChecked, setIsChecked] = useState(false);

  const handleCheckboxClick = () => {
    setIsChecked((prev) => !prev);
  };

  return (
    <div>
      <label className="flex gap-x-2">
        <span className=" after:content-[':']">show todo list</span>
        <input
          type="checkbox"
          checked={isChecked}
          onClick={handleCheckboxClick}
        />
      </label>
      {isChecked && (
        <Suspense fallback={<div>Loading...</div>}>
          <Component />
        </Suspense>
      )}
    </div>
  );
};

/** 子コンポーネントではTodoの取得が成功していることが約束される **/
const Component = () => {
  const { data: todos } = useSuspenseQuery({
    queryKey: ["todos"],
    queryFn: getTodos,
  });

  return (
    todos.length > 0 && (
      <ul>
        {todos.map((todo) => (
          <li key={todo.id}>{todo.title}</li>
        ))}
      </ul>
    )
  );
};

export { TodoList };

queryFnでnullを返す

条件を満たすまでの間、queryFnでnullを返すという方法もあります。この場合は todos の型がnullになり得るので、条件式の追加が必要になります。

@@ -36,20 +36,21 @@ const TodoList = () => {
       </label>
       {isChecked && (
         <Suspense fallback={<div>Loading...</div>}>
-          <Component />
+          <Component shouldShowTodos={isChecked} />
         </Suspense>
       )}
     </div>
   );
 };

-const Component = () => {
+const Component = ({ shouldShowTodos }: { shouldShowTodos: boolean }) => {
   const { data: todos } = useSuspenseQuery({
     queryKey: ["todos"],
-    queryFn: getTodos,
+    queryFn: () => (shouldShowTodos ? getTodos() : null),
   });

   return (
+    todos &&
     ^^^^ 型が Todo[] | nullになるため、ハンドリングが必要
     todos.length > 0 && (
       <ul>
         {todos.map((todo) => (

useQueryを使う場合

useQuery を使う場合は enabled オプションで関数の実行を制御できます。
ただ、Suspenseが利用できる場合は積極的に useSuspenseQuery を活用した方が良さそうです。

"use client";

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

type Todo = {
  id: number;
  userId: number;
  title: string;
  completed: boolean;
};

const getTodos = async () => {
  const res = await fetch("https://jsonplaceholder.typicode.com/todos");
  const todos = (await res.json()) as Todo[];

  return todos;
};

export const TodoList = () => {
  const [isChecked, setIsChecked] = useState(false);
  const { data: todos, isLoading } = useQuery({
    queryKey: ["todos"],
    queryFn: getTodos,
    enabled: isChecked,
  });

  const handleCheckboxClick = () => {
    setIsChecked((prev) => !prev);
  };

  return (
    <div>
      <label className="flex gap-x-2">
        <span className=" after:content-[':']">show todo list</span>
        <input
          type="checkbox"
          checked={isChecked}
          onClick={handleCheckboxClick}
        />
      </label>
      {todos && todos.length > 0 && (
        <ul>
          {todos.map((todo) => (
            <li key={todo.id}>{todo.title}</li>
          ))}
        </ul>
      )}
      {isLoading && <div>Loading...</div>}
    </div>
  );
};

おわりに

今回はTanStack Queryを使って条件付きフェッチを実装する方法を紹介しました。Suspenseは便利な機能ですが、コンポーネントを適切に分割することが求められます。Reactの考え方に沿ったコンポーネント設計がより重要になっていると感じました。

https://github.com/yuki-yamamura/tanstack-query-conditional-fetching-with-suspense

frontend flat

Discussion