Skip to content

hydrate() throws on undefined dehydratedState since query-core 5.102.1 (guard removed) — breaks TanStack Start SSR streaming, silently no-ops all mutations #11307

Description

@pwnchief

Summary

hydrate() in @tanstack/query-core throws TypeError: Cannot read properties of undefined (reading 'mutations') when called with undefined. A defensive guard that made this a no-op was removed in @tanstack/query-core@5.102.1 and is still absent in the latest (5.102.5). This breaks TanStack Start SSR streaming: the client QueryClient never finishes hydrating and every mutation silently no-ops (no network request fired) while SSR markup and query polling keep working, so the failure is easy to miss.

The regression

hydrate() used to bail early on a non-object/null argument. That guard is gone as of 5.102.1.

query-core@5.102.0hydration.js (guard present):

function hydrate(client, dehydratedState, options) {
  if (typeof dehydratedState !== "object" || dehydratedState === null) return; // <-- guard
  const mutationCache = client.getMutationCache();
  // ...
  dehydratedState.mutations?.forEach(/* ... */);
}

query-core@5.102.1..5.102.5 — guard deleted:

function hydrate(client, dehydratedState, options) {
  const mutationCache = client.getMutationCache();  // <-- straight in
  // ...
  dehydratedState.mutations?.forEach(/* ... */);    // throws when dehydratedState === undefined
}

Verified against the published packages (grep for dehydratedState !== "object" in build/modern/hydration.js): 5.102.0 → 1 match, 5.102.1 → 0, 5.102.5 → 0.

Who calls hydrate(client, undefined)

The TanStack Start SSR-query integration (@tanstack/router-ssr-query-core, currently frozen at 1.169.1, pulled in via @tanstack/react-router-ssr-query) installs a client-side router.options.hydrate that reads a stream of dehydrated query chunks and calls hydrate on every read — including the terminal { done: true, value: undefined } read — before it checks done:

// @tanstack/router-ssr-query-core@1.169.1/dist/esm/index.js
const reader = dehydrated.queryStream.getReader();
reader.read().then(async function handle({ done, value }) {
  hydrate(queryClient, value, hydrateOptions);   // value === undefined on the terminal read
  if (done) return;                              // done-check happens AFTER hydrate()
  return handle(await reader.read());
}).catch((err) => {
  console.error("Error reading query stream:", err);
});

On the terminal read value is undefined. With the old guard, hydrate(client, undefined) was a safe no-op and the stream drained cleanly. Without it, hydrate dereferences undefined.mutations and throws. The throw is swallowed by the .catch (console.error("Error reading query stream:", …)), so hydration silently aborts partway: the client QueryClient is left un-hydrated and subsequent mutations never issue a network request — no error surfaced to the app, SSR HTML and polling queries still work, so it looks like the app is fine until you notice writes do nothing.

Symptom as seen in an app

Error reading query stream: TypeError: Cannot read properties of undefined (reading 'mutations')
    at hydrate (query-core hydration.js)
    at handle (@tanstack/react-router-ssr-query index.js)

(In a production/minified build this also shows up as React error #418 — hydration mismatch — because hydration bailed early.)

Reproduction

  • A TanStack Start app using setupRouterSsrQueryIntegration (i.e. @tanstack/react-router-ssr-query@tanstack/router-ssr-query-core@1.169.1).
  • Any @tanstack/query-core >= 5.102.1 resolved for the client QueryClient.
  • SSR-render a route, let the dehydrated query stream drain on the client → the terminal hydrate(client, undefined) throws → mutations silently stop firing.

Pinning @tanstack/query-core (and @tanstack/react-query) back to 5.102.0 restores the guard and fixes it.

Environment

  • @tanstack/query-core: 5.102.1 – 5.102.5 (broken); 5.102.0 (works)
  • @tanstack/react-query: 5.102.x
  • @tanstack/react-router-ssr-query / @tanstack/router-ssr-query-core: 1.167.1 / 1.169.1
  • @tanstack/react-start: 1.167.1, @tanstack/react-router: 1.168.1
  • React 19, Node 24, SSR

Suggested fixes (either resolves it)

  1. Restore the guard in query-core's hydrate() — bail when dehydratedState is not a non-null object. This is the historical, defensive behavior and matches the direction of fix(examples/vue/nuxt3): guard hydrate() against a null dehydrated state #11270 (guarding hydrate() against null/undefined dehydrated state in the Nuxt example). hydrate(client, undefined) being a no-op is a reasonable contract.
  2. Have the SSR-query bridge check done before calling hydrate — on the terminal read value is undefined, so hydrate should not be called for it. (The bridge is currently frozen, so a fix here alone wouldn't help existing consumers on the pinned bridge version — hence option 1 is the more robust fix.)

Happy to open a PR restoring the guard if that's the preferred direction.

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