Hacktoberfest 2026:メンテナが10月に向けて印を付けた、オープンで初心者向けの issue。 Hacktoberfest の issue を見る

HydrationBoundary ignores/skips server-prefetched query when a parent useQuery with same key is rendered before child useSuspenseQuery

オープン
#10,145 コメント 4 件 リアクション 1 件 担当者 0 名 GitHub で見る

メンテナーはふだん 1 日以内に返信

まだ誰も着手していません。

評価

難易度
4/5
見積もり時間
3〜5日
初心者へのやさしさ
52/100
issue の種類
バグ
明瞭さ
おおむね明確
活発さ
活発
技術スタック
react, typescript
領域
frontend

調査の方向性

リンクされているCodeSandboxの再現から始め、レポートで説明されているapp/(dashboard)/todos/[todoId]/layout.tsxまたはpage.tsxのフローを使って、親のuseQueryと子のuseSuspenseQuery周辺のHydrationBoundaryの動作を追跡します。サーバーでのprefetch、dehydration、hydrationの状態を比較します。完了の条件は、重複したSSRリクエストやclient-request guard errorを発生させずに、prefetched keyが子で引き続き利用可能であることです。

索引モデルが issue の本文から書いたものです。

説明

Describe the bug

If a useQuery is rendered before a HydrationBouhndary, hydrations for that key will be skipped despite the fact that the useQuery would have not fetched in the first place.

In our case we have a prefetch where we pass the dehydrated queryClient to a HydrationBoundary. But there is a useQuery using the same key as the prefetched query above the hydration boundary that (in some global component higher up) gets rendered. The hydration boundary skips this hydration and the child components of the HydrationBoundary that have useSuspenseQuery for that key still run their queries in SSR.

Small example:

function Page({ params }) {
  const queryClient = getQueryClient();
  void queryClient.prefetchQuery(
    buildTodoQueryOptions(params.todoId, makeServerRequest),
  );

  return (
    <>
      <SomeHeaderComponent /> {/* a `useQuery` lives in here */}
      <HydrationBoundary state={dehydrate(queryClient)}>
        <MyChildComponent /> {/* a `useSuspenseQuery` lives in here and fires despite the above prefetch */}       
      </HydrationBoundary>
    </>
  );
}

Also, in our particular case this triggers a guard we have to ensure we do not client fetch on the server during SSR. This also means the request is being duplicated and firing twice (in our prefetch and during SSR, which should have been skipped)

Flow (all on the server):

  1. Parent/route boundary prefetches the detail query todoId on the server.
  2. A higher component in layout/header renders a useQuery with the key todoId.
  3. Pass the dehydrated and prefetched queryClient to a HydrationBoundary lower than the useQuery.
  4. The HydrationBoundary` skips this key and does not hydrate the prefetched query as expected.
  5. Child useSuspenseQuery then attempts a fetch with client requestor during SSR and hits the guard error.
Repro

Server prefetch at detail layout/page:

// In app/(dashboard)/todos/[todoId]/layout.tsx or page.tsx
function Page({ params }) {
const queryClient = getQueryClient();
void queryClient.prefetchQuery(buildTodoQueryOptions(todoId, makeServerRequest));
return <HydrationBoundary state={dehydrate(queryClient)}>{children}</HydrationBoundary>;
}

Global/header client component that also reads URL todoId (lives OUTSIDE/above the above layout.tsx)

"use client";

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

function TodoHeaderMenu() {
  const params = useParams();
  const todoId = typeof params.todoId === "string" ? params.todoId : "";

  const { data: todo } = useQuery({
    ...buildTodoQueryOptions(todoId),
    enabled: Boolean(todoId),
  });

  return <span>{todo?.title}</span>;
}

Child detail component:

"use client";

function TodoPage({ todoId }: { todoId: string }) {
  const { data: todo } = useSuspenseQuery(buildTodoQueryOptions(todoId));
  return <TodoDetails todo={todo} />;
}

buildTodoQueryOptions defaults to client requestor. Client-request guard throws:

if (typeof window === "undefined") {
  throw new Error(
    "Warning, aborting SSR. You attempted to fetch from the server with clientRequest."
  );
}

AI disclosure: I had codex help me with the quick repro.

Questions
  1. Is this current behavior expected?
  2. Should hydration logic be changed to only skip queries that are fetching (aka don't skip useQueries that fill the cache key on the server but do not initiate filling the value on the server)
Your minimal, reproducible example

https://codesandbox.io/p/devbox/pr7c4g

Steps to reproduce

The home page on that will bring up the error in the next dev error popup

Expected behavior

I would expect a useQuery in a higher component with the same key to not STOP the server fetch/hydration given useQuery would not trigger the client request guard like useSuspenseQuery does. useQuery does not start the queryFn on the server, so it should not stop useSuspenseQueries that CAN imo.

This also comes with the additional problem that because of the useQuery, the useSuspenseQuery is firing again, ruining deduplication.

How often does this bug happen?

Every time

Screenshots or Videos

No response

Platform

macOS

Chromium

next: 16.1.1

Tanstack Query adapter

react-query

TanStack Query version

5.90.21

TypeScript version

No response

Additional context

I can technically gate the useQuery in the parent component with something like

const isDealQueryReady = !!(
    state &&
    state.data !== undefined &&
    state.status !== "pending" &&
    state.fetchStatus !== "fetching"
  )

//and then 
if (!isDealQueryReady) {
  return null;
}

//...

//...`useQuery`

and then I do not get the error but that is obnoxious.

主要言語
TypeScript
スター
50.4k
フォーク
4.2k
平均マージ
14時間 36分
マージ済み PR(30日)
331

環境構築

はじめの一歩

  1. issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
  2. 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
  3. リポジトリをフォークし、ブランチを切って変更します。
  4. issue 番号を参照したプルリクエストを送ります。

TanStack/query のほかの issue

TanStack/query の issue をすべて見る

似ている issue

TypeScript の issue をもっと見る

新しい issue をメールで受け取る

初心者向けの GitHub issue を短くまとめたダイジェスト。