Skip to content

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

Closed
KATT wants to merge 5 commits into
cursor/experimental-query-handles-271efrom
cursor/query-handles-devtools-271e
Closed

KATT wants to merge 5 commits into
cursor/experimental-query-handles-271efrom
cursor/query-handles-devtools-271e

Conversation

@KATT

@KATT KATT commented Nov 1, 2026 •

Copy link
Copy Markdown
Owner

This PR builds on #2 (experimental query handles) and targets its branch. It's a separate PR to keep #2'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 #2 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).
Open in Web Open in Cursor 

@github-actions

github-actions Bot commented Nov 1, 2026 •

Copy link
Copy Markdown

🚀 Changeset Version Preview

3 package(s) bumped directly, 21 bumped as dependents.

🟨 Minor bumps

Package Version Reason
@tanstack/query-core 5.105.2 → 5.106.0 Changeset
@tanstack/query-devtools 5.105.2 → 5.106.0 Changeset
@tanstack/react-query 5.105.2 → 5.106.0 Changeset
@tanstack/angular-query-experimental 5.105.2 → 5.106.0 Dependent
@tanstack/eslint-plugin-query 5.105.2 → 5.106.0 Dependent
@tanstack/preact-query 5.105.2 → 5.106.0 Dependent
@tanstack/preact-query-devtools 5.105.2 → 5.106.0 Dependent
@tanstack/preact-query-persist-client 5.105.2 → 5.106.0 Dependent
@tanstack/query-async-storage-persister 5.105.2 → 5.106.0 Dependent
@tanstack/query-broadcast-client-experimental 5.105.2 → 5.106.0 Dependent
@tanstack/query-persist-client-core 5.105.2 → 5.106.0 Dependent
@tanstack/query-sync-storage-persister 5.105.2 → 5.106.0 Dependent
@tanstack/react-query-devtools 5.105.2 → 5.106.0 Dependent
@tanstack/react-query-next-experimental 5.105.2 → 5.106.0 Dependent
@tanstack/react-query-persist-client 5.105.2 → 5.106.0 Dependent
@tanstack/solid-query 5.105.2 → 5.106.0 Dependent
@tanstack/solid-query-devtools 5.105.2 → 5.106.0 Dependent
@tanstack/solid-query-persist-client 5.105.2 → 5.106.0 Dependent
@tanstack/vue-query 5.105.2 → 5.106.0 Dependent

🟩 Patch bumps

Package Version Reason
@tanstack/lit-query 0.2.33 → 0.2.34 Dependent
@tanstack/svelte-query 6.3.8 → 6.3.9 Dependent
@tanstack/svelte-query-devtools 6.3.8 → 6.3.9 Dependent
@tanstack/svelte-query-persist-client 6.3.8 → 6.3.9 Dependent
@tanstack/vue-query-devtools 6.3.8 → 6.3.9 Dependent

@cursor
cursor Bot force-pushed the cursor/query-handles-devtools-271e branch 3 times, most recently from 86bcf1d to 0b6a5aa Compare November 2, 2026 01:03
@cursor
cursor Bot force-pushed the cursor/query-handles-devtools-271e branch from 0b6a5aa to b842101 Compare November 2, 2026 14:11
@cursor
cursor Bot force-pushed the cursor/query-handles-devtools-271e branch 5 times, most recently from 83ccbd6 to d778587 Compare November 2, 2026 19:48
cursoragent and others added 5 commits November 2, 2026 20:00
…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>
@cursor
cursor Bot force-pushed the cursor/experimental-query-handles-271e branch from e3a2b96 to b6bc296 Compare November 2, 2026 20:00
@cursor
cursor Bot force-pushed the cursor/query-handles-devtools-271e branch from d778587 to f639acc Compare November 2, 2026 20:02
@KATT

KATT commented Nov 3, 2026

Copy link
Copy Markdown
Owner Author

Moved to TanStack#12513.

@KATT KATT closed this Nov 3, 2026
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