Repository navigation
Conversation
🚀 Changeset Version Preview2 package(s) bumped directly, 22 bumped as dependents. 🟨 Minor bumps
🟩 Patch bumps
|
cursor
Bot
force-pushed
the
cursor/experimental-query-handles-271e
branch
from
November 1, 2026 23:54
facad3e to
07baafb
Compare
3 of 5 tasks
cursor
Bot
changed the base branch from
cursor/hydrated-rejection-retry-271e
to
master
November 1, 2026 23:55
3 of 5 tasks
…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>
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>
cursor
Bot
force-pushed
the
cursor/experimental-query-handles-271e
branch
from
November 2, 2026 20:00
e3a2b96 to
b6bc296
Compare
Owner
Author
|
Moved to TanStack#12512. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This PR doesn't depend on #1 and shares no code with it, so it targets
master. #3 adds the devtools support on top of this branch.🎯 Changes
This PR adds query handles to query-core as
experimental_APIs, and react-query re-exports them. Every existing API works as before; the new behavior only applies to queries that these functions start.A route loader or Server Component starts queries from a query factory (a function that takes one input, or none, and returns query options). It then passes a small serializable handle (
{ queryKey, input }) to the components that read the queries.query-core
experimental_prefetchHandle(qc, factory, input)starts a query without waiting and doesn't reject: fetch errors show up where a component reads the handle.experimental_ensureHandlewaits for any data: it resolves at once with stale cached data and refetches in the background.experimental_fetchHandlewaits for fresh data. All three callqueryClient.query, orqueryClient.infiniteQuerywhen the options have aninitialPageParam, and they followstaleTimethe same way on server and browser clients.{ queryKey, input }. A handle fromexperimental_ensureHandleorexperimental_fetchHandlemeans the cache had the query's data when the loader finished; the handle itself carries no data.queryOptions(input, opts?), but passing them is a type error, because the handle only carries the input andexperimental_fromHandlecouldn't pass the rest on.experimental_fromHandle(factory, handle)rebuilds the options, and adds nothing to them. You spread it and add read-site options likeselect, which the factory types. Aselectinside the factory works too. Readers get the data from the cache, souseQuerytypesdataasTData | undefined, as usual, anduseSuspenseQuerygives defined data.as constkeeps them apart). Only the types check this; nothing checks at runtime. The handle's key type drops theDataTagand keeps optional elements.experimental_waterfallHandlebuilds a handle for a query whose input you only know during render. It's a plain function, not a hook, and it starts nothing: the component that reads the handle fetches the query, asuseQueryoruseSuspenseQueryalways does. It has two forms:While the new input maps to the same key (by the factory's
queryKeyHashFn), the second form returns the handle you passed in, so the first page reads the query the loader started. That also keeps the old input when only fields outside the key change, so everything thequeryFnreads belongs in the key.experimental_InferQueryHandleData<typeof postQuery>gives the data a reader gets, without calling a hook, for example to type a child component's prop. It's the same type asuseSuspenseQuery'sdata: what the factory'sselectreturns, or else thequeryFn's data, which isInfiniteDatafor an infinite factory. It takes a factory type or a handle type.experimental_QueryHandle,experimental_QueryHandleFactory,experimental_FromHandleOptionsandexperimental_InferQueryHandleData. The tRPC-like proxy tests below build on the first three alone. The other helper types stay internal, so they can change while the API is experimental.react-query
No new hooks: react-query re-exports the handle functions from query-core, and they work with every existing hook.
Docs
A new guide,
framework/react/guides/query-handles, with a nav entry. It covers:fromHandlewith read-site options, andexperimental_waterfallHandleselectinfers both at the read site and inside the factory, and that a read-siteselectreplaces the factory'sexperimental_InferQueryHandleDatafor typing a prop that takes the datauseSuspenseQuerygives defined data whileuseQuerydata stays possiblyundefined, since the cache may lose the data after the loader ranshouldDehydrateQuery, and that this needs a transport that can send promisesstaleTime: this works as before, so withstaleTime: 0the browser refetches on mount. The guide shows the double fetch and how to avoid it.cache(), or next to the request'sQueryClient). The guide also shows a module-level version to avoid and explains why it breaks.Tests
handles.test.tsx, including bothexperimental_waterfallHandleforms, a customqueryKeyHashFn,experimental_fetchHandlewith an infinite factory, the three start functions on a server client, awaited handles that are plain{ queryKey, input },experimental_fromHandleadding noinitialData, and a reader with no data afterqueryClient.clear()queryHandles.test.tsxstreams a pending prefetched query throughdehydrateandHydrationBoundaryto auseSuspenseQueryreader, which doesn't fetch it again.waterfallHandle.test.tsxcovers a reader that fetches once, a loader's handle that stays until the new input maps to another query, and an awaited handle's cached data on the first render.queryHandles.test-d.tsxcovers input types, a factory's extra parameters (@ts-expect-error), optional inputs, optional key elements,useQuerydata that is possiblyundefinedanduseSuspenseQuerydata that is defined for handles from every verb, the same for infinite queries, keys that keep factories apart, and both waterfall forms.queryHandles.test-d.tsxalso checksselectwithuseQuery,useSuspenseQueryand the infinite hooks. A read-siteselectspread over the options gets thequeryFn's data with no annotation, and read-siteplaceholderDataandenabledkeep their types. Aselectinside the factory types the readers' data, works with all four handle functions, and leaves the cache data typed as thequeryFn's data. A read-siteselectover a factory that already selects replaces it and gets thequeryFn's data.queryHandles.test-d.tsxandtrpcLikeProxy.test-d.tsxcheck thatexperimental_InferQueryHandleDataequalsuseSuspenseQuery's oruseSuspenseInfiniteQuery'sdatafor a plain factory, a selecting factory, an infinite factory, an infinite factory withselect, a handle type and a tRPC-like procedure, and that it types a prop whichuseQuery's possibly undefineddatadoesn't fit.trpcLikeProxy.test.tsxandtrpcLikeProxy.test-d.tsxbuild a small RPC-style options proxy on the public API alone, to check that integrations like tRPC's can build on these functions. They test the start functions on the server, a JSON round trip throughdehydrateandhydrate, a reader that finds the procedure from the handle's key, defined data throughuseSuspenseQuery, a spreadselect,experimental_waterfallHandleswitching, and an infinite procedure that loads two pages up front.usinginstead ofbeforeEach,afterEachor manual calls:using _timers = fakeTimers(),using queryClient = clearOnDispose(new QueryClient()),using _subscription = subscribe(observer, noop)andusing rendered = unmountOnDispose(renderWithClient(...)). This PR adds these four helpers, each with a test, to@tanstack/query-test-utils; the first three match fix(query-core): use a hydrated query's current 'retry' options when it rejects #1's copies byte for byte. Declare a render after its client, and the render unmounts before the client clears.✅ Checklist
pnpm run test:pr, or these tests do not apply to this pull request. (Ran oxlint, knip,test:typesacross all TS versions, and vitest for query-core, react-query and query-test-utils, plus the docs link check.)🚀 Release Impact