Skip to content

HydrationBoundary ignores/skips server-prefetched detail query when a parent useQuery with same key is rendered before child suspense fetch #10145

@ColemanDunn

Description

@ColemanDunn

Describe the bug

We have an id-based route where parent/global components can see the id from URL and call useQuery for the detail resource, but suspended/SSR-safe child components use useSuspenseQuery for the same query key. The render of the useQuery causes the hydration boundary to skip hydration for the child using the useSuspenseQuery and 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:

  1. Parent/route boundary prefetches the detail query on server.
  2. A higher component in layout/header still renders useQuery with enabled: Boolean(id).
  3. On SSR, HydrationBoundary does not hydrate/replace the prefetched query as expected.
  4. 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
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:

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions