🐧

ReactQuery で use がサポートされるようになるらしい

2024/10/12に公開

Tanstack Queryの5.59.0のリリースでuseQueryとReact19のuseがexperimentalで一緒に使用できるようになったみたいです。

実際に使ってみる

Reactのプロジェクトを作成後、React19の機能を使用するためにpackage.jsonを以下に変更します。

{
  "dependencies": {
    "@types/react": "npm:types-react@beta",
    "@types/react-dom": "npm:types-react-dom@beta"
  },
  "overrides": {
    "@types/react": "npm:types-react@beta",
    "@types/react-dom": "npm:types-react-dom@beta"
  }
}

次に、Tanstack Queryのv5.59.0を追加し、QueryClientProviderのセットアップを行います。

準備が完了したら、useQueryとuseを一緒に使ってみます。

const getComments = async () => {
  await new Promise((resolve) => setTimeout(resolve, 3000));
  return {
    data: [
      {
        id: "1",
        name: "John Doe",
      },
      {
        id: "2",
        name: "Jane Doe",
      },
      {
        id: "3",
        name: "John Smith",
      },
    ],
  };
};

function App() {
  return (
    <div>
      <Suspense fallback={<div>Loading...</div>}>
        <CommentsComponent />
      </Suspense>
    </div>
  );
}

const CommentsComponent = () => {
  const { promise } = useQuery({
    queryKey: ["comments"],
    queryFn: getComments,
    experimental_prefetchInRender: true,
  });
  const { data } = use(promise);

  return (
    <div>
      {data.map((comment) => (
        <div key={comment.id}>{comment.name}</div>
      ))}
    </div>
  );
};

export default App;

useQueryのexperimental_prefetchInRenderのoptionをtrueにし、返されたpromiseをuseに渡してあげることで、Suspenseと一緒に使うことができました。

何が嬉しいのか

Tanstack QueryでもuseSuspenseQueryがあるので、Suspenseと一緒に使うことはできます。

function App() {
  return (
    <div>
      <Suspense fallback={<div>Loading...</div>}>
        <CommentsComponent />
      </Suspense>
    </div>
  );
}

const CommentsComponent = () => {
  const {
    data: { data },
  } = useSuspenseQuery({
    queryKey: ["comments"],
    queryFn: getComments,
  });
  // const { promise } = useQuery({
  //   queryKey: ["comments"],
  //   queryFn: getComments,
  //   experimental_prefetchInRender: true,
  // });
  // const { data } = use(promise);

  return (
    <div>
      {data.map((comment) => (
        <div key={comment.id}>{comment.name}</div>
      ))}
    </div>
  );
};

export default App;

しかし、useUseSuspenseQueryを共通のSuspense内で複数使うとwaterfallが発生する問題がありました。

例えば、CommentsComponent内でkeyが異なるqueryを呼び出した場合、ローディング時間が3+3=6秒になってしまいます。

const CommentsComponent = () => {
  const {
    data: { data: data1 },
  } = useSuspenseQuery({
    queryKey: ["comments1"],
    queryFn: getComments,
  });

  const {
    data: { data: data2 },
  } = useSuspenseQuery({
    queryKey: ["comments2"],
    queryFn: getComments,
  });

  return (
    <div>
      {data1.map((comment) => (
        <div key={comment.id}>{comment.name}</div>
      ))}
      {data2.map((comment) => (
        <div key={comment.id}>{comment.name}</div>
      ))}
    </div>
  );
};

上記の場合だとuseSuspenseQueriesを使うことで解決できますが、Suspense内に複数のコンポーネントが存在し、それぞれのコンポーネント内でuseSuspenseQueryが使用されている場合もwaterfallが発生します(もちろんそれぞれのコンポーネントをSuspenseで囲めば問題は解決しますが、useSuspenseQueryを使用するたびにwaterfall問題を意識する必要があり、意図せずwaterfallが発生してしまうリスクがあります)。

そこでuseと一緒に使うとwaterfallを気にする必要がなくなります。

function App() {
  const { promise: promise1 } = useQuery({
    queryKey: ["comments1"],
    queryFn: getComments,
    experimental_prefetchInRender: true,
  });

  const { promise: promise2 } = useQuery({
    queryKey: ["comments2"],
    queryFn: getComments,
    experimental_prefetchInRender: true,
  });

  return (
    <div>
      <Suspense fallback={<div>Loading...</div>}>
        <CommentsComponent promise1={promise1} promise2={promise2} />
      </Suspense>
    </div>
  );
}

const CommentsComponent = ({
  promise1,
  promise2,
}: {
  promise1: Promise<{ data: Comment[] }>;
  promise2: Promise<{ data: Comment[] }>;
}) => {
  const { data: data1 } = use(promise1);

  const { data: data2 } = use(promise2);

  return (
    <div>
      {data1.map((comment) => (
        <div key={comment.id}>{comment.name}</div>
      ))}
      {data2.map((comment) => (
        <div key={comment.id}>{comment.name}</div>
      ))}
    </div>
  );
};

また、useは他のhookとは違いif文の中でも呼べるので特定の条件でだけAPIを呼び出したい時にも以下のように書くことができます。

const CommentsComponent = ({
  promise1,
  promise2,
}: {
  promise1: Promise<{ data: Comment[] }>;
  promise2: Promise<{ data: Comment[] }>;
}) => {
  const { data: data1 } = use(promise1);

  if (Math.random() > 0.5) {
    const { data: data2 } = use(promise2);

    return (
      <div>
        {data1.map((comment) => (
          <div key={comment.id}>{comment.name}</div>
        ))}
        {data2.map((comment) => (
          <div key={comment.id}>{comment.name}</div>
        ))}
      </div>
    );
  }

  return (
    <div>
      {data1.map((comment) => (
        <div key={comment.id}>{comment.name}</div>
      ))}
    </div>
  );
};

まとめ

まだexperimentalですが、useQueryとuseを一緒に使うことができるようになりそうです。useSuspenseQueryのwaterfall問題の解消であったり、条件分岐内でAPIリクエストが行えるようになったりと今までの辛みを解消してくれそうですね。

Discussion