Skip to content

feat(query-core, query-devtools): show how query handles started their queries - #12513

Draft
KATT wants to merge 17 commits into
TanStack:mainfrom
KATT:cursor/query-handles-devtools-271e
Draft

KATT wants to merge 17 commits into
TanStack:mainfrom
KATT:cursor/query-handles-devtools-271e

Conversation

@KATT

@KATT KATT commented Nov 3, 2026

Copy link
Copy Markdown
Contributor

Note

We made this PR with AI help and haven't yet done the two steps in TanStack's AI Contribution Policy. We still need to check that we understand all the code, and to rewrite this description in our own words.

Builds on #12512. Review only the commits after b6bc296.

This is a separate PR to keep #12512's core changes small.

🎯 Changes

Core records start info only in development. In a production build, the handle functions return the factory's options untouched, add no cache listener and wrap no queryFn, and experimental_getQueryStartInfo returns undefined.

query-core

  • In development, the handle functions record start info on the query: which function started it, on the server or the client, when, and whether an observer has read it since. The info lives under a non-enumerable Symbol.for('@tanstack/query-core/experimental_start') key on the Query object, because the devtools bundle their own copy of query-core and wouldn't see a module-level map. experimental_getQueryStartInfo(query) reads it, and experimental_QueryStartInfo types it. A start on fresh data fetches nothing, so recording it emits an observerResultsUpdated cache event, which tells the devtools to read the new record.
  • experimental_waterfallHandle starts nothing, so in development core keeps its handles in a module-level WeakSet instead. The handles stay plain { queryKey, input } objects with no symbol properties, so a Server Component can pass them to a Client Component. For a handle in the set, experimental_fromHandle wraps the options' queryFn, so the query records the reader's first fetch with the verb waterfallHandle. Refetches don't overwrite that record, and when the switch form returns the loader's own handle, the loader's record stays.
  • In development, dehydrate includes the start info as experimental_start, and hydrate restores it with observed: false, so a hydrated query keeps the server's record.
  • In development, dehydrate on the server warns once when it leaves out a pending query that experimental_prefetchHandle, experimental_ensureHandle or experimental_fetchHandle started there, since the browser would fetch it again. Queries an earlier dehydrate call sent don't count, so the guide's nested-dehydration pattern and ReactQueryStreamedHydration don't warn, and neither does a browser dehydrate of a hydrated query. The warning belongs here rather than in feat(query-core, react-query): add experimental query handles #12512 because it needs the start info to know which function started the query, and where.

query-devtools

  • A waterfall badge, also in the row's aria-label, on queries that a reader of an experimental_waterfallHandle handle first fetched.
  • "Started by: prefetchHandle (server)" in Query Details.
  • A "Prefetched, never read" note on queries that a handle function started and no observer has read.
  • The devtools read start info with experimental_getQueryStartInfo from their own copy of query-core, which finds the same registered symbol. The build only bundles that one function.

Docs

The query handles guide gains a sentence on the dev warning, a mention of the waterfall badge, and a Devtools section, which says that production builds record nothing, so the devtools show none of it there.

Tests

  • query-core: queryStartInfo.test.tsx covers recording and observed, the waterfall reader's first fetch, waterfall handles with no symbol properties, a switched-back loader handle, the cache event for a start on fresh data, start info that resetQueries keeps, queries that other APIs start, the record moving from server to client, and when the warning shows: once per query, not for queries an earlier dehydrate sent, and not when the browser dehydrates a hydrated query. Two tests stub NODE_ENV to production and check that nothing is recorded, the queryFn isn't wrapped, and the cache has no listener. The DehydratedQuery key-set type test covers the new field.
  • react-query: the first waterfallHandle test also checks that the query records a suspense reader's fetch and marks it as read.
  • query-devtools: the badge, "Started by" (client and hydrated server), a new start on fresh data, the never-read note, and no hints on other queries.
  • The tests clean up with using: subscribe(observer, noop) unsubscribes observers, unmountOnDispose(...) unmounts renders, the console.warn spies dispose themselves, and a productionEnvironment() helper restores NODE_ENV. query-devtools now uses @tanstack/query-test-utils as a dev dependency instead of its own clearOnDispose.

Screenshots

These come from a small React app running in development mode. Its loader awaits the viewer with experimental_ensureHandle, and prefetches page 1 of the posts and the stats with experimental_prefetchHandle. The page reads the viewer and the posts with experimental_fromHandle, and never reads the stats. "Next page" switches the posts handle with experimental_waterfallHandle.

Before the switch, Query Details shows that prefetchHandle started page 1:

Query Details for page 1 of the posts, started by prefetchHandle on the client

The loader prefetched the stats, but no component read them:

Query Details for the stats, with the Prefetched, never read note

After the switch, page 2 has the waterfall badge, and Query Details shows that waterfallHandle started it:

Page 2 of the posts with the waterfall badge, started by waterfallHandle on the client

The whole flow:

query-handles-devtools-flow.mp4

✅ Checklist

  • I have followed the steps in the Contributing guide.
  • I have tested code changes locally with pnpm run test:pr, or these tests do not apply to this pull request. (Ran oxlint, knip, test:types across all TS versions, and vitest for query-core, query-devtools, react-query and query-test-utils, plus the docs link check.)
  • I have followed the AI contribution policy and fully understand the code in this pull request, including any code generated with AI assistance.

🚀 Release Impact

  • This change affects published code, and I have generated a changeset.
  • This change is docs/CI/dev-only (no release).

cursoragent and others added 17 commits November 2, 2026 19:56
…bles for 'using' in tests

Co-authored-by: alexander <alexander@n1s.se>
…eWaterfall'

Co-authored-by: alexander <alexander@n1s.se>
Co-authored-by: alexander <alexander@n1s.se>
…' with a plain 'experimental_waterfallHandle' whose reader starts the query

Co-authored-by: alexander <alexander@n1s.se>
Adds an 'unmountOnDispose' helper to @tanstack/query-test-utils, so a render declared after its client unmounts before the client is cleared, and splits the helper tests into one file per helper.

Co-authored-by: alexander <alexander@n1s.se>
… copy

Co-authored-by: alexander <alexander@n1s.se>
Also keep optional elements in a handle's key type, and test streaming a
pending query, infinite fetchHandle and server clients. The guide now
covers promise transports and data that goes out twice.

Co-authored-by: alexander <alexander@n1s.se>
Keep experimental_QueryHandle, experimental_QueryHandleWithData,
experimental_QueryHandleFactory and experimental_FromHandleOptions. The
guide and tests now cover a handle's data seeding the cache again after
clear(), removeQueries or resetQueries, as any initialData does.

Co-authored-by: alexander <alexander@n1s.se>
Every handle is now a plain { queryKey, input }. experimental_ensureHandle
and experimental_fetchHandle still wait for the data, but return the same
handle as experimental_prefetchHandle, and experimental_fromHandle adds no
initialData. Readers that need defined data use useSuspenseQuery.

Co-authored-by: alexander <alexander@n1s.se>
…Handle

Covers a read-site select spread over the options, a select inside the
factory, and a read-site select over a factory that already selects, for
useQuery, useSuspenseQuery and the infinite hooks.

Co-authored-by: alexander <alexander@n1s.se>
It gives the data a reader of a factory's query gets, the same type as
useSuspenseQuery's data, so a component can type a prop that takes it.

Co-authored-by: alexander <alexander@n1s.se>
…r queries in the devtools

Co-authored-by: alexander <alexander@n1s.se>
…nfo tests with 'using'

Adds the 'subscribe' helper to @tanstack/query-test-utils, as in the hydrated rejection retry PR, and uses query-test-utils in query-devtools instead of a local 'clearOnDispose'. Console spies are held with 'using' too.

Co-authored-by: alexander <alexander@n1s.se>
… copy

Co-authored-by: alexander <alexander@n1s.se>
…n less

Track waterfall handles in a WeakSet, so a Server Component can pass them
on. dehydrate now warns once per query, only on the server, and not for
queries an earlier dehydrate sent. A start on fresh data notifies the
cache, so the devtools show it, and hydrate only restores start info in
development.

Co-authored-by: alexander <alexander@n1s.se>
Production builds add no cache listener and wrap no queryFn.

Co-authored-by: alexander <alexander@n1s.se>
@changeset-bot

changeset-bot Bot commented Nov 3, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: f639acc

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 24 packages
Name Type
@tanstack/query-core Minor
@tanstack/react-query Minor
@tanstack/query-devtools Minor
@tanstack/angular-query-experimental Minor
@tanstack/lit-query Patch
@tanstack/preact-query Minor
@tanstack/query-async-storage-persister Minor
@tanstack/query-broadcast-client-experimental Minor
@tanstack/query-persist-client-core Minor
@tanstack/query-sync-storage-persister Minor
@tanstack/solid-query Minor
@tanstack/svelte-query Patch
@tanstack/vue-query Minor
@tanstack/react-query-devtools Minor
@tanstack/react-query-next-experimental Minor
@tanstack/react-query-persist-client Minor
@tanstack/preact-query-devtools Minor
@tanstack/solid-query-devtools Minor
@tanstack/svelte-query-devtools Patch
@tanstack/vue-query-devtools Patch
@tanstack/preact-query-persist-client Minor
@tanstack/solid-query-persist-client Minor
@tanstack/svelte-query-persist-client Patch
@tanstack/eslint-plugin-query Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coderabbitai

coderabbitai Bot commented Nov 3, 2026

Copy link
Copy Markdown
Contributor

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

CodeRabbit Verify

  • Run CodeRabbit Verify to test the changes in this pull request.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants