Repository navigation
fix(tanstack-query): give skipToken queries a distinct query key - #2210
Conversation
`skipToken` is a symbol, and both TanStack's default `hashKey` and the
documented `RPCJsonSerializer`-based `queryKeyHashFn` drop symbol values.
A query skipped with `input: skipToken` therefore hashed to the same key
as the same procedure called without input, and the two observers shared
one `Query`. The skipped observer's throwing `queryFn` could win, so the
next invalidation put the enabled query into an error state.
`generateOperationKey` now writes `skipToken: true` in place of `input`
when the input is `skipToken`. Partial-match keys such as `.key()` and
`.key({ type })` still match skipped queries, while `.key({ input })`
does not.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DGZkM7MFXNDHJBg9SmARgj
Reuse the module-level QueryClient in the observer tests (retry already defaults to 0 outside the browser) and register unsubscribes with onTestFinished so they also run when an assertion fails. Narrow the key tests to what the fix changes: the skipToken marker, the skipped vs. no-input hash under both hashers, and partial matching. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DGZkM7MFXNDHJBg9SmARgj
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DGZkM7MFXNDHJBg9SmARgj
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
orpc | 3a775a8 | Commit Preview URL Branch Preview URL |
Oct 12 2026, 08:43 AM |
More templates
@orpc/ai-sdk
@orpc/arktype
@orpc/bun
@orpc/client
@orpc/cloudflare
@orpc/contract
@orpc/experimental-effect
@orpc/evlog
@orpc/hibernation
@orpc/json-schema
@orpc/experimental-lock
@orpc/experimental-msw
@orpc/nest
@orpc/next
@orpc/node
@orpc/openapi
@orpc/opentelemetry
@orpc/pinia-colada
@orpc/pino
@orpc/publisher
@orpc/ratelimit
@orpc/server
@orpc/shared
@orpc/swr
@orpc/tanstack-query
@orpc/trpc
@orpc/valibot
@orpc/zod
commit: |
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
There was a problem hiding this comment.
ℹ️ No critical issues — the
input: skipTokencollision is fixed correctly. One sibling disabling path (queryFn: skipToken) is not covered.
Reviewed changes
This PR gives input: skipToken queries a distinct query key so they no longer share a Query with the same procedure's no-input query.
generateOperationKeywrites askipToken: truemarker —key.tsnow emits{ skipToken: true }in place ofinputwhen the input is theskipTokensymbol;OperationKeygains the optional field.- Key unit tests —
key.test.tsasserts the marker, that TanStack'shashKeyand the docs'RPCJsonSerializerhasher both distinguish skipped from no-input keys, and that partial matching is unchanged. - Observer-level regression tests —
procedure-utils.test.tsverifiesqueryOptionsandinfiniteOptionskeep two cache entries and the enabled query stayssuccessafterinvalidateQueries. - E2E test —
tests/e2e.test.tsxrendersstreamedOptions()andstreamedOptions({ input: skipToken })together and asserts the same.
deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏
Merging this PR will not alter performance
|

Problem
skipTokenis aSymbol, and both TanStack's defaulthashKey(JSON.stringify) and theRPCJsonSerializer-basedqueryKeyHashFnfrom the docs drop symbol values. So these two hashed to the same key and shared oneQuery:TanStack lets the most recent observer's options win (
QueryObserver.setOptions→query.setOptions), so the shared query picks up the skipped observer's queryFn, which always throwsqueryFn should not be called when skipToken used for skipping.enabled: falseonly applies to the skipped observer. The enabled observer keeps the shared query active, so the nextinvalidateQueries/refetchQueriesruns the throwing queryFn and the enabled query ends up inerror.This hits the pattern the docs recommend (
input: search ? { search } : skipToken) whenever another component uses the same procedure without input. It affectsqueryOptions,streamedOptions,liveOptionsandinfiniteOptions(for infinite queries, when the input function returnsundefinedfor the initial page).Fix
generateOperationKeynow writesskipToken: truein place ofinputwhen the input isskipToken, and theOperationKeytype gains an optionalskipTokenfield.orpc.planet.key()and.key({ type: 'query' })still match skipped queries, and.key({ input: {...} })does not.generateOperationKeyis still called with{ type, input: skipToken }, andoptions.queryKeyis still its return value, so the existing spy assertions hold.enabled: falseand the throwing guard in the queryFn are unchanged.Pinia Colada and SWR have no
skipTokensupport, so they are not affected.Tests
The new tests were written first and fail without the fix:
key.test.ts: theskipToken: truemarker; skipped vs. no-input hashes differ under bothhashKeyand the docs'RPCJsonSerializerhasher; partial matching still works.procedure-utils.test.ts:QueryObserver(queryOptions) andInfiniteQueryObserver(infiniteOptions) repros: the cache holds two queries, and the enabled one stayssuccessafterinvalidateQueries(it previously ended inerror).tests/e2e.test.tsx: twouseQueryhooks withstreamedOptions()andstreamedOptions({ input: skipToken })in onerenderHook.Validation
pnpm vitest run packages/tanstack-query: 127 passedpnpm type:check: cleanpnpm lint: clean🤖 Generated with Claude Code
https://claude.ai/code/session_01DGZkM7MFXNDHJBg9SmARgj
Generated by Claude Code