Skip to content

feat(tanstack-query)!: type-aware structural sharing and serialized input keys - #2305

Merged
dinwwwh merged 7 commits into
mainfrom
claude/structural-sharing-date-map-set-178e4d
Oct 23, 2026
Merged

dinwwwh merged 7 commits into
mainfrom
claude/structural-sharing-date-map-set-178e4d

Conversation

@dinwwwh

@dinwwwh dinwwwh commented Oct 22, 2026 •

Copy link
Copy Markdown
Member

Query results now keep their previous references when a refetch or live event returns equal data, including Date, URL, Map, Set and custom serializer types, so components stop re-rendering on every update. Query keys now store the input in serialized form, so inputs with these types hash and invalidate by value with TanStack's default hash, and createQueryKeyHashFn is no longer needed.

const orpc = createTanstackQueryUtils(client, { serializer }) // e.g. the RPCSerializer your link uses

orpc.planet.list.queryOptions({ input, structuralSharing: false }) // per-call opt-out

Changes

  • .queryOptions, .liveOptions and .infiniteOptions default to createStructuralSharing(serializer). For queries not built with oRPC utils, use createStructuralSharing() in QueryClient defaults.
  • Date, URL, Set and Map are compared directly, so sharing is exact with any serializer. Custom types are shared when an RPC serializer serializes them the same; with a JSON-only serializer they are always replaced, since equal JSON doesn't prove equal values.
  • New serializer option on router and procedure utils, next to prefix, matching pinia-colada (feat(pinia-colada): configurable key serializer and type-aware input keys #2300). Contract meta can't set it.
  • Keys keep each non-JSON value's type in inputMeta, keyed by path, so a Date and its ISO string get different keys and partial keys still match. An OpenAPIJsonSerializer also works as the key serializer when keys don't need types.
  • Docs: new Key Serializer, Structural Sharing and OpenAPI JSON Serializer sections; Pinia Colada's Key Serializer section is shortened to match; the SSR section, serializer docs, the Next.js SSR blog post and playground no longer use createQueryKeyHashFn.

Fixes

  • Set and Map inputs no longer share one cache entry under the default hash, and BigInt inputs no longer throw.
  • invalidateQueries({ queryKey: orpc.x.key({ input: { from } }) }) only matches queries with an equal from, instead of every query with a Date, Set or Map there.

Breaking

  • createQueryKeyHashFn is removed. Drop queryKeyHashFn from the QueryClient; keep createHydrationOptions for SSR.
  • Keys for inputs containing Date, BigInt, URL, Set, Map or custom types change shape. Hand-built keys or predicates that read queryKey[…].input now see JSON values. Keys for plain-JSON inputs are unchanged.
  • Structural sharing is set per query, so it takes precedence over defaultOptions.queries.structuralSharing on the QueryClient. Override it per call, in scoped options or with a plugin.

Known limitations

  • A partial key built from a JSON value (e.g. an ISO string) also matches queries whose input had a Date there, since plain values carry no type info. .key() input is typed from the procedure input, so this only shows up with Date | string inputs or casts.

Performance

  • Faster than TanStack's own replaceEqualDeep on plain JSON: about 7 ms vs 12 ms for 10k equal rows, and 8 ms vs 11 ms when every row changed.
  • Copies are built only once something differs, and maps are walked without entry arrays: 10k rows with Dates take about 4 ms, a 1k-entry Map about 0.2 ms.

Testing

  • tanstack-query and pinia-colada unit and e2e tests pass, including partial matching and invalidation against a real QueryClient, and OpenAPI serializers for keys and structural sharing. 100% coverage on the structural sharing module.
  • Package and test type checks, lint, verify-docs-jsdoc and blume validate --strict pass.

…nput keys

Query results now keep their previous references when a refetch or live
event returns equal data, including Date, Map, Set and custom serializer
types, so components no longer re-render on every update. `.queryOptions`,
`.liveOptions` and `.infiniteOptions` default to `createStructuralSharing`.

Query keys now store the serialized input plus `inputMeta` (ported from
pinia-colada #2300), driven by a new `serializer` option, so inputs with
these types hash and partially match by value with TanStack's default hash.

BREAKING CHANGE: `createQueryKeyHashFn` is removed. Keys for inputs that
contain Date, BigInt, URL, Set, Map or custom types change shape.
@codecov

codecov Bot commented Oct 22, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@pkg-pr-new

pkg-pr-new Bot commented Oct 22, 2026 •

Copy link
Copy Markdown
More templates

@orpc/ai-sdk

npm i https://pkg.pr.new/@orpc/ai-sdk@2305

@orpc/arktype

npm i https://pkg.pr.new/@orpc/arktype@2305

@orpc/bun

npm i https://pkg.pr.new/@orpc/bun@2305

@orpc/client

npm i https://pkg.pr.new/@orpc/client@2305

@orpc/cloudflare

npm i https://pkg.pr.new/@orpc/cloudflare@2305

@orpc/contract

npm i https://pkg.pr.new/@orpc/contract@2305

@orpc/experimental-effect

npm i https://pkg.pr.new/@orpc/experimental-effect@2305

@orpc/evlog

npm i https://pkg.pr.new/@orpc/evlog@2305

@orpc/hibernation

npm i https://pkg.pr.new/@orpc/hibernation@2305

@orpc/json-schema

npm i https://pkg.pr.new/@orpc/json-schema@2305

@orpc/experimental-lock

npm i https://pkg.pr.new/@orpc/experimental-lock@2305

@orpc/mcp

npm i https://pkg.pr.new/@orpc/mcp@2305

@orpc/msw

npm i https://pkg.pr.new/@orpc/msw@2305

@orpc/nest

npm i https://pkg.pr.new/@orpc/nest@2305

@orpc/next

npm i https://pkg.pr.new/@orpc/next@2305

@orpc/node

npm i https://pkg.pr.new/@orpc/node@2305

@orpc/openapi

npm i https://pkg.pr.new/@orpc/openapi@2305

@orpc/opentelemetry

npm i https://pkg.pr.new/@orpc/opentelemetry@2305

@orpc/pinia-colada

npm i https://pkg.pr.new/@orpc/pinia-colada@2305

@orpc/pino

npm i https://pkg.pr.new/@orpc/pino@2305

@orpc/publisher

npm i https://pkg.pr.new/@orpc/publisher@2305

@orpc/ratelimit

npm i https://pkg.pr.new/@orpc/ratelimit@2305

@orpc/server

npm i https://pkg.pr.new/@orpc/server@2305

@orpc/shared

npm i https://pkg.pr.new/@orpc/shared@2305

@orpc/swr

npm i https://pkg.pr.new/@orpc/swr@2305

@orpc/tanstack-query

npm i https://pkg.pr.new/@orpc/tanstack-query@2305

@orpc/trpc

npm i https://pkg.pr.new/@orpc/trpc@2305

@orpc/valibot

npm i https://pkg.pr.new/@orpc/valibot@2305

@orpc/zod

npm i https://pkg.pr.new/@orpc/zod@2305

commit: 0fa0916

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
orpc 0fa0916 Commit Preview URL

Branch Preview URL
Oct 23 2026, 03:52 AM

Comment thread packages/tanstack-query/src/key.ts
Comment thread packages/tanstack-query/src/structural-sharing.ts Outdated
@macroscopeapp

macroscopeapp Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

Approvability

Verdict: Needs Human Approval

Macroscope's review found this PR requires human approval — The PR changes default TanStack Query behavior and cache-key semantics across existing query, live, and infinite paths, while also removing createQueryKeyHashFn. Its new structural-sharing algorithm and breaking key/API contract are substantial enough to require human review.

You can add or adjust custom eligibility rules. Learn more.

…ects in structural sharing

A changed null-prototype dictionary was copied into `{}`, so the result
inherited Object.prototype and keys like `toString` resolved to functions.
@codspeed

codspeed Bot commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

1 warning

⚠️ 5 benchmarks are dominated by system calls

System calls cannot be simulated consistently and are left out of the measure. The reported value understates the real cost. Switch to the Walltime instrument to measure them.

Learn more about system calls

See affected benchmarks

✅ 28 untouched benchmarks
⏩ 2 skipped benchmarks1


Comparing claude/structural-sharing-date-map-set-178e4d (0fa0916) with main (4c6b6b3)

Open in CodSpeed

Footnotes

  1. 2 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports. ↩

Comment thread packages/tanstack-query/src/structural-sharing.ts Outdated

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ No blocking issues — one documentation note inline.

Reviewed changes

  • Serialized input keys (key.ts): generateOperationKey now stores input in serialized form plus inputMeta (per-path serializer types) so keys hash and partially match by value; createQueryKeyHashFn is removed and a new serializer option is accepted.
  • Structural sharing (structural-sharing.ts, new): createStructuralSharing(serializer) forks TanStack's replaceEqualDeep with a Date fast path and a serializer-backed comparison for Map/Set/custom types.
  • Options plumbing (procedure-utils.ts, router-utils.ts, shared-utils.ts, meta.ts): the new serializer flows through procedure/router/scoped options and the contract meta excludes it; .queryOptions/.liveOptions/.infiniteOptions default to the new structuralSharing, .streamedOptions/.mutationOptions do not.
  • Docs/playground (tanstack-query.mdx, serializer.mdx, Next.js SSR blog + playground): createQueryKeyHashFn removed, new "Key Serializer" and "Structural Sharing" sections.
  • Tests: new structural-sharing.test.ts plus key/procedure-utils/router-utils/shared-utils coverage of serializer + sharing.

I traced the replaceEqualDeep fork against TanStack's implementation and the repo's serializeJson: the aSerialized.json === a "no handler" branch only ever returns b (a missed share, never stale data), maps/blobs are redundant because every Blob also appears in json, and the proto/depth guards are conservative. The package tests pass (118), and an additional 20k-case fuzz of the new module found no invariant violations.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

Comment thread apps/content/docs/integrations/tanstack-query.mdx Outdated
…izer

The Date fast path compared timestamps only, so a custom date handler that
also serializes own fields (like a zone) kept a stale Date when only that
field changed. Dates without own enumerable keys keep the fast path.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ The follow-up commit is correct — no new issues. The earlier documentation note still applies.

Reviewed changes (since 2b226f71)

  • structural-sharing.ts: changed objects now copy with Object.create(proto) instead of {}, so a deserialized null-prototype object keeps its prototype when a subtree changes; ordinary objects keep the {} fast path.
  • structural-sharing.test.ts: added coverage asserting the null prototype is preserved, toString stays absent, and unchanged nested values still share.

The fix is sound: the copy branch is only reached for isPlainObject values, so proto is either Object.prototype (fast path) or a null-prototype/constructor-less prototype, and Object.create(proto) reproduces it faithfully while setOwn continues to guard the __proto__ key. Package tests pass.

Pullfrog  | Fix it ➔ | View workflow run | Using deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ The follow-up fix is correct — no new issues. The earlier documentation note still applies.

Reviewed changes (since 79c67cdc)

  • structural-sharing.ts: the Date fast path is now gated on !hasOwnEnumerableKey(a) && !hasOwnEnumerableKey(b), so a Date that carries own enumerable fields falls through to replaceEqualSerialized where a custom date handler can compare them; a new hasOwnEnumerableKey helper uses for...in plus Object.hasOwn.
  • structural-sharing.test.ts: added coverage for a custom date handler that serializes own fields, and for inherited enumerable Date.prototype keys being ignored.

The change is correct: with the default serializer a Date's own fields are not part of the wire format (only the ISO string is), so sharing by time is unchanged, while a custom handler now sees the own fields. Object.hasOwn inside the for...in loop correctly ignores inherited keys. Tests pass.

Pullfrog  | Fix it ➔ | View workflow run | Using deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

Add an OpenAPI JSON Serializer section like the RPC one, and shorten the
TanStack Query and Pinia Colada Key Serializer sections: pass a custom RPC
JSON Serializer, or an OpenAPI JSON Serializer when keys don't need types.
Tests cover OpenAPI serializers as key serializers in both packages.
…izer

Date, URL, Set and Map are compared directly, so results are exact even with
a JSON-only serializer like OpenAPIJsonSerializer, which serializes Set([1n])
and Set(['1']) the same. Custom types are shared only when the serializer
reports type info, and the serializer is no longer defaulted.

Copies are built lazily, arrays and objects get their own loops, and maps are
walked without entry arrays: 37-47% faster on 10k-row payloads, 76% on maps.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No new issues found.

Reviewed changes

  • Structural sharing rewritten for speed and any-serializer exactness (structural-sharing.ts): replaceEqualDeep now dispatches to dedicated paths — replaceEqualBuiltIn (Date by getTime, URL by href, Set/Map element-wise, each gated on no own enumerable keys), replaceEqualArray, replaceEqualObject, and replaceEqualSerialized for custom types.
  • Lazy copy construction (copyArrayPrefix/copyObjectPrefix, assign): shallow copies are built only once a value actually differs, with __proto__ still routed through setOwn.
  • Serializer path tightened (replaceEqualSerialized): the "no handler" test moved from json === a to an empty meta, and meta is compared with stringifyJSON.
  • OpenAPI JSON serializer documented and accepted as a serializer (openapi-json-serializer.ts JSDoc + docs/tests): usable as a key serializer; its serializeJson carries no meta, so with it custom types are never shared and keys carry no type info.

I traced the new branch structure, re-checked isPlainObject equivalence (proto === Object.prototype || proto === null || !proto.constructor), and ran the package tests (43 pass in structural-sharing + key). A 40k-case in-repo fuzz of the new createStructuralSharing() over plain/Date/URL/Set/Map data found no violations of either invariant — result === a ⇒ deeply equal and deeply equal ⇒ result === a. The prior documentation thread is already resolved.

Pullfrog  | View workflow run | Using deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

@dinwwwh
dinwwwh merged commit af4f625 into main Oct 23, 2026
14 checks passed
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.

1 participant