Skip to content

feat(solid-query): rewrite the adapter onto Solid 2.0's native async model - #11308

Open
ryansolid wants to merge 13 commits into
TanStack:solid-query-v6-prefrom
ryansolid:feat/solid-query-v6-native-rewrite
Open

feat(solid-query): rewrite the adapter onto Solid 2.0's native async model#11308
ryansolid wants to merge 13 commits into
TanStack:solid-query-v6-prefrom
ryansolid:feat/solid-query-v6-native-rewrite

Conversation

@ryansolid

@ryansolid ryansolid commented Aug 26, 2026

Copy link
Copy Markdown

Target branch: solid-query-v6-pre (@tanstack/solid-query@6.x, Solid 2)

What this is

A ground-up rewrite of the Solid Query adapter onto Solid 2.0's native
async model. The v6-pre adapter still mirrors the React adapter's
architecture — observer notifications driving a store, tracking proxies,
manually-thrown NotReadyErrors to mimic suspension — which fights the
framework: Solid 2 components don't re-render, its reactive graph already
knows how to suspend on a pending promise, hold previous values through
refetches, route rejections to error boundaries, and serialize settled
async values for streaming hydration. The rewrite hands those jobs to the
framework and keeps TanStack Query for what the framework does not do:
the cache, key identity, staleness/GC policy, retry/dedupe/cancellation,
devtools, and the ecosystem contract.

The result deletes the notification/store layer entirely:
createStore/reconcile, tracking proxies, notifyManager batching, and
observer-notification coupling are gone, replaced by
memo/projection/signal/action/optimistic primitives. QueryObserver and
MutationObserver survive only as lifecycle/policy engines (GC pinning,
mount-fetch policy, refetch intervals, focus/reconnect) with noop
listeners.

Architecture

Reads (useBaseQuery)

One memo is the data node: it returns either a settled value or the live
fetch promise, and the engine does the rest — first loads suspend into
<Loading>, refetches hold the previous committed value, rejections
surface to <Errored>, and on the server the settled value serializes
into the hydration payload.

  • Subscription model: one version signal per hook, bumped by cache events
    for the query's hash (committed OR latest-computed hash — they diverge
    during a held transition, and settle events for a new key land before
    the hold commits). Every derived read pulls fresh state from the cache
    through it.
  • Pull model: an enabled pending-idle query is fetched by the tracked
    read itself (the router query() model). This is what revives queries
    whose enabling flips while parked under a suspended boundary, where
    effects cannot run. q.fetch dedupes against policy-driven fetches;
    observer options are synced before pulling so the deferred options
    effect sees a no-op diff.
  • The data node IS a single auto-reconciling store projection, identical
    on both sides (the Solid Router "feed query() into createProjection"
    shape): the derive asks the cache for the answer and the engine owns
    everything else — the server runs it, waits, and serializes the settled
    value (deferStream holds the flush); pending reads suspend into
    <Loading>; a hydrating client adopts the serialized value with
    default ('server') semantics — server truth owns the DOM for the whole
    hydration window, and cache writes that land mid-stream commit when the
    stream closes via Solid's hydration-end divergence takeover (requires
    solid-js > 2.0.0-rc.3, where a latched node's mid-stream divergence is
    deferred rather than lost). Deep reads are fine-grained (a
    component reading data[0].name re-runs only when that leaf changes),
    and every landing — refetch, invalidation, placeholder upgrade —
    reconciles into the existing proxy graph instead of replacing it, so
    item identity survives across fetches. The identity key is the
    reconcile option (default 'id', function or null for positional).
    Primitives ride a wrapper leaf. A per-hook primed gate keeps the
    pull-model fetch from racing hydration: until the hook's own serialized
    node entry has primed the cache (or was found absent), the read parks
    instead of fetching data the stream already carries.
  • Scalar metadata is a createProjection reconciled per-field from query
    state — a component reading only isFetching re-runs on fetchStatus
    flips, not every state transition.
  • isFetching is the OR of two channels: committed fetchStatus (first
    loads, imperative reads) and the engine's pending probe (refetches
    inside held transitions, where committed state cannot flip mid-hold by
    design).
  • Stable promise identity across recomputes (chainOnce), so mid-flight
    version bumps don't hand the engine a new pending promise per recompute.
  • A reactive queryClient accessor is honored mid-life: the version
    subscription and observer are re-pointed when the client changes.
  • refetch() syncs the observer to the latest computed options at call
    time — under a held transition the setOptions render effect is
    deferred, and an imperative refetch must target what the UI is
    currently asking for.

SSR + streaming hydration

Cache transfer is content-addressed — the Solid Router query() pattern,
one hydration story, no adapter-owned channel. On the server the provider
serializes every query the request touches into Solid's hydration
registry under sq:<queryHash> ({ data, t: dataUpdatedAt }, raw
pre-select data). Serialization happens at fetch-DISPATCH time, while the
request's serialization context is live, by handing seroval the fetch
promise; the payload streams whenever the fetch settles, and entries the
data nodes also serialized are emitted once (seroval cross-reference
dedupe). On the client each hook looks its own hash up in the registry,
reconstructs the cache entry through query-core hydrate() — staleness
policy intact — and only then attaches its observer. Settled entries
prime synchronously at setup; streamed entries prime as their chunk
lands, per query, so early-flushed components go live while later
boundaries still stream.

Content addressing means coverage is the cache's, not the rendered
tree's: a query prefetched in a loader and never read by any component
still transfers, and — like the router — the registry outlives the
hydration window, so a component mounted long after (lazy route,
post-hydration navigation) adopts the server's payload instead of
refetching. Entries are consumed one-shot. External hydrate() hosts
(the TanStack Start pattern) coexist: re-priming the same entry is silent
under newer-wins.

Contracts pinned by the SSR fixture tests:

  • Settled-only serialization: boundaries guarantee only settled state
    serializes, and every exposed field honors it. Server meta reads of an
    unsettled enabled query tie themselves to the data node and suspend;
    isFetching/isPending can never serialize true; disabled-pending
    serializes as-is ('pending' IS its settled truth).
  • Hydration id parity: every reactive node a hook creates exists on
    both server and client (some inert), because hydration id assignment is
    positional.
  • Fetch gates: no fetch is ever started inside the hydration window
    or while a persister is restoring; values come from adoption or node
    priming, and the post-attach observer issues any genuinely-needed fetch
    after the window closes.
  • Cross-cache aggregates (useIsFetching, useIsMutating,
    useMutationState): serialize their empty value (a hydrating client
    can only observe 0/[] at claim time) and stay latched through the
    hydration window, going live from the first post-hydration effect.

Mutations (useMutation)

Rides Solid core's action primitive: one transaction across the async
gap, optimistic overlays with automatic rollback, and atomic settle.
Lifecycle callbacks are stripped from core's Mutation.execute and
re-run inside the transaction post-yield, preserving query-core's
callback-rejection semantics (success-path callback failures fail the
mutation; error-path failures report unhandled without displacing the
original error). Invalidation-triggered refetches hold the settle
transition until fresh data lands — mutation settle and cache refresh
commit together.

mutate returns the mutation promise; mutateAsync is removed (there is
no longer a fire-and-forget vs promise split to justify two entry
points).

The rest

  • useQueries: N base-query layers under repeat (positional identity),
    combined through a memo; the queries array is reactive in length and
    content.
  • useInfiniteQuery: same read layer with _type: 'infinite' stamped so
    reactive key changes keep infiniteQueryBehavior; pagers are memos over
    the same entry.
  • Counting hooks share one internal helper (createCacheAggregate):
    a durable signal + optimistic override (cache events firing inside an
    action transaction stay observable mid-flight; out-of-transaction
    writes stay durable) with the hydration latch described above.

Surface changes (breaking)

  • data is non-optional on query results: reads suspend (or throw to the
    boundary) instead of ever returning undefined. The <Show when={q.data}> loading-guard idiom is dead code now — JSX reads reach
    parity with plain Solid async reads.
  • mutateAsync removed; mutate returns a promise.
  • reconcile option re-shaped: it is now the reconciliation KEY for the
    data store (string | (item => any) | null, default 'id') — Solid 2's
    projections auto-reconcile their returns, so the v5 mechanism (manual
    reconcile() merges into an adapter-owned store, with a
    structuredClone seed hack) is core behavior now. The v5 function form
    (oldData, newData) => newData is gone; core structuralSharing still
    covers cache-level referential stability underneath.
  • suspense option removed: suspension is the model, and boundaries are
    the control point.
  • The create* runtime aliases (createQuery, createMutation,
    createQueries, createInfiniteQuery, createIsFetching,
    createIsMutating, createMutationState) and the Create* type
    aliases are removed, completing the deprecation from fix(solid-query): deprecate create in favor of use, and add full docs #8950use* is
    the only naming, matching the docs.
  • deferStream is now actually implemented — on the v6 line it was
    declared in the types but wired to nothing. It passes straight through
    to Solid 2's per-computation deferStream option on the data node: the
    server holds the stream flush until the query resolves instead of
    letting the surrounding boundary render its fallback (engine behavior,
    covered by solid core's stream tests; ignored on the client).
  • placeholderData keeps React semantics (client-progressive, no server
    fetch, isPlaceholderData provenance flag, masked 'success' status)
    and composes with SSR: the placeholder serializes, the client shows it
    through hydration and fetches for real after its window closes.

Intended divergences (documented in-tests)

  • A cancelled first load cannot stay idle while an enabled query is
    actively read: the pull model re-demands the value (it.skip with
    rationale in useQuery.test.tsx). Unmount, disable, or remove the
    query to stop it.
  • Mid-flight mutation state (isPaused, failureCount) inside an action
    transaction holds its pre-flight face until settle — held updates
    commit atomically. The mutation cache remains imperatively inspectable
    mid-flight.

Verification

  • Full package suite green, vitest typecheck and eslint clean, tsc --build clean. Suites were ported test-by-test with per-file porting
    notes; tests asserting store/observer mechanics (render counts,
    notification batching) were dropped, semantics tests were kept or
    rewritten to DOM-level assertions.
  • New semantics suites for reads, mutations, queries-array, and infinite
    queries; SSR fixture harness (real vite server/client builds, jsdom
    hydrate() against real server HTML + streamed payload) covering
    string SSR, streaming with held-open boundaries, fully-buffered late
    hydration, prefetched-never-rendered transfer with post-hydration
    adoption, external-hydrate() coexistence (TanStack Start pattern),
    and the settled-only serialization contracts.
  • Template acceptance: the Solid 2.0 fullstack TanStack template
    (solid-js rc.0 and rc.3, production SSR server) with this adapter
    swapped in — suite green, hydration clean, and single-flight mutations
    verified end-to-end in a browser (login/rename/logout flows, mutation
    responses refreshing multiple queries with zero invalidation calls and
    zero extra fetches).
  • Size: comment-stripped source is ~1,770 lines — smaller than the React
    adapter's core (~1,850), which additionally ships its suspense hook
    surface and Next.js-only streaming SSR as separate packages; this
    adapter's streaming hydration is included and framework-agnostic. The
    primitive inventory is strictly simpler.

Notes

  • Requires the next solid-js release (> 2.0.0-rc.3) for one core piece
    this PR leans on: the hydration-end divergence takeover (a latched
    node's mid-stream cache write commits when the stream closes instead of
    being lost). It is on solid's next branch; CI will fail against
    published rc.3 until it ships. The adapter performs no server reactive
    writes, per rc.3's deprecation.
  • The devtools package is untouched; a follow-up could surface the
    action-transaction view.
  • useQueries rows ride the same per-hook node priming automatically
    (each row is a full base-query layer).

Reads become a single async memo over the query cache (suspends into
<Loading>, holds previous data through refetches, routes rejections to
<Errored>, serializes settled values for streaming hydration); mutations
ride Solid core's `action` primitive with transactional settle and
optimistic overlays; cross-cache hooks share a dual-write aggregate
with a hydration latch. The observer notification/store layer
(createStore/reconcile, tracking proxies, notifyManager batching) is
deleted — QueryObserver/MutationObserver remain as lifecycle/policy
engines with noop listeners.

Breaking: result `data` is non-optional (reads suspend instead of
returning undefined), mutateAsync is removed (mutate returns a
promise), and the reconcile/suspense/deferStream options are removed.

SSR contracts pinned by the fixture suite: settled-only serialization
for every exposed field, hydration id parity, no fetches inside the
hydration/restore windows, and hydration-latched cross-cache
aggregates. Validated against solid-js 2.0.0-rc.0 and rc.3, including
an end-to-end template run with single-flight mutations.

Co-authored-by: Cursor <cursoragent@cursor.com>
@coderabbitai

coderabbitai Bot commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 10a51e16-a60e-4f51-a7ed-17ca5e3d9287

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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

Comment @coderabbitai help to get the list of available commands.

@nx-cloud

nx-cloud Bot commented Aug 26, 2026

Copy link
Copy Markdown

View your CI Pipeline Execution ↗ for commit 94480d6

Command Status Duration Result
nx affected --targets=test:sherif,test:knip,tes... ✅ Succeeded 4m 10s View ↗
nx run-many --target=build --exclude=examples/*... ✅ Succeeded 2s View ↗

☁️ Nx Cloud last updated this comment at 2026-08-27 12:19:32 UTC

@socket-security

socket-security Bot commented Aug 26, 2026

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Added@​solidjs/​web@​2.0.0-rc.3991008397100
Updatedbabel-preset-solid@​2.0.0-rc.0 ⏵ 2.0.0-rc.21001009096 +1100
Addedsolid-js@​2.0.0-rc.31001009596100

View full report

@pkg-pr-new

pkg-pr-new Bot commented Aug 26, 2026

Copy link
Copy Markdown
More templates

@tanstack/angular-query-experimental

npm i https://pkg.pr.new/@tanstack/angular-query-experimental@11308

@tanstack/eslint-plugin-query

npm i https://pkg.pr.new/@tanstack/eslint-plugin-query@11308

@tanstack/lit-query

npm i https://pkg.pr.new/@tanstack/lit-query@11308

@tanstack/preact-query

npm i https://pkg.pr.new/@tanstack/preact-query@11308

@tanstack/preact-query-devtools

npm i https://pkg.pr.new/@tanstack/preact-query-devtools@11308

@tanstack/preact-query-persist-client

npm i https://pkg.pr.new/@tanstack/preact-query-persist-client@11308

@tanstack/query-async-storage-persister

npm i https://pkg.pr.new/@tanstack/query-async-storage-persister@11308

@tanstack/query-broadcast-client-experimental

npm i https://pkg.pr.new/@tanstack/query-broadcast-client-experimental@11308

@tanstack/query-core

npm i https://pkg.pr.new/@tanstack/query-core@11308

@tanstack/query-devtools

npm i https://pkg.pr.new/@tanstack/query-devtools@11308

@tanstack/query-persist-client-core

npm i https://pkg.pr.new/@tanstack/query-persist-client-core@11308

@tanstack/query-sync-storage-persister

npm i https://pkg.pr.new/@tanstack/query-sync-storage-persister@11308

@tanstack/react-query

npm i https://pkg.pr.new/@tanstack/react-query@11308

@tanstack/react-query-devtools

npm i https://pkg.pr.new/@tanstack/react-query-devtools@11308

@tanstack/react-query-next-experimental

npm i https://pkg.pr.new/@tanstack/react-query-next-experimental@11308

@tanstack/react-query-persist-client

npm i https://pkg.pr.new/@tanstack/react-query-persist-client@11308

@tanstack/solid-query

npm i https://pkg.pr.new/@tanstack/solid-query@11308

@tanstack/solid-query-devtools

npm i https://pkg.pr.new/@tanstack/solid-query-devtools@11308

@tanstack/solid-query-persist-client

npm i https://pkg.pr.new/@tanstack/solid-query-persist-client@11308

@tanstack/svelte-query

npm i https://pkg.pr.new/@tanstack/svelte-query@11308

@tanstack/svelte-query-devtools

npm i https://pkg.pr.new/@tanstack/svelte-query-devtools@11308

@tanstack/svelte-query-persist-client

npm i https://pkg.pr.new/@tanstack/svelte-query-persist-client@11308

@tanstack/vue-query

npm i https://pkg.pr.new/@tanstack/vue-query@11308

@tanstack/vue-query-devtools

npm i https://pkg.pr.new/@tanstack/vue-query-devtools@11308

commit: 64bd7a1

@brenelz

brenelz commented Aug 26, 2026

Copy link
Copy Markdown
Collaborator

Looks like some tests failed. The main query one passed though. Just some surrounding ones
https://cloud.nx.app/runs/DjhRqLThLP?utm_source=pull-request&utm_medium=comment

…n SSR option

deferStream was declared in the option types on this line but wired to
nothing. Solid 2 exposes it as a creation-time memo option, so pass it
straight through to the data node: the server holds the stream flush
until the query resolves instead of flushing the surrounding boundary's
fallback. Server-only; ignored on the client.

Co-authored-by: Cursor <cursoragent@cursor.com>
@brenelz

brenelz commented Aug 26, 2026

Copy link
Copy Markdown
Collaborator

I tested this in the fullstack-tanstack template and seems to function better

ryansolid and others added 10 commits August 26, 2026 12:02
…ction

Solid 2 projections auto-reconcile their returns by key, so the data face
becomes a store: deep reads are fine-grained and item identity survives
across refetches (keyed by the new `reconcile` option, default 'id').
The async memo remains the SSR/hydration spine underneath; the projection
derive is inert on the server — server projections run eagerly at
creation, so a live derive would fetch at hook creation and serialize a
duplicate payload, and the serialized entry would latch the hydrating
client's projection for the whole stream. With nothing serialized the
client projection computes live through the hydration window.

Rewrites the structural-sharing test to the stronger store contract and
adds leaf-granularity, custom-key, and primitive-data semantics tests.

Co-authored-by: Cursor <cursoragent@cursor.com>
- align @solidjs/web, babel-preset-solid and solid-js dev ranges across the
  solid packages (sherif flagged the split, and devtools/persist-client were
  resolving a second solid-js copy)
- drop the unused `export` on the internal BaseQueryLayer seam (knip)
- port the PersistQueryClientProvider suite to the 2.0 read layer: a query
  with nothing cached now suspends for the whole restore window, so the
  tests assert the visible fallback -> restored -> refreshed transitions
  and the fetch/callback bookkeeping instead of observer result snapshots

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ction

Collapse the three-node data face (serialized async memo + transparent
hydration memo + client-only projection) into one createProjection with
an identical derive on both sides — the Solid Router feed-query-into-
projection shape. The engine owns SSR serialization (deferStream rides
the projection now), suspense, hydration adoption, and keyed
reconciliation. ssrSource 'hybrid' keeps hydrated components live: the
serialized value claims the DOM and the derive re-takes over from the
live cache per hydrated region. A per-hook `primed` signal gates the
pull-model fetch so the hydration takeover recompute cannot race data
the channel is about to deliver (SSR-errored queries fall through and
fetch normally once the channel declines them).

Co-authored-by: Cursor <cursoragent@cursor.com>
… data node

Drop ssrSource 'hybrid' from the data projection: query data IS
server-serialized truth, so the serialized value owns the DOM for the
whole hydration window. Mid-stream cache writes commit when the stream
closes through Solid's hydration-end divergence takeover (solid-js >
2.0.0-rc.3 — earlier engines lose a latched node's mid-stream divergence
instead of deferring it). Network activity is unchanged: observers
attach per-query as the channel primes, so mid-stream invalidations
still refetch immediately; only the DOM commit defers to hydration end.

Co-authored-by: Cursor <cursoragent@cursor.com>
… the transport

Delete the provider-owned dehydration channel and its coordinator. The
data projection's serialized root now carries the cache facts hydration
needs ({ value, t: dataUpdatedAt, raw?: pre-select data }); on a hydrated
mount each hook peeks its projection's node id, loads that entry itself,
primes the query cache through query-core hydrate() (staleness intact,
newer-wins, silent under external pre-priming), and attaches its observer
per query as chunks land. One serialization story, engine-owned; ~240
lines deleted. Requires solid-js > 2.0.0-rc.3 (peekNextChildId export).

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
…uery() pattern)

Serialize cache entries under sq:<queryHash> at fetch-dispatch time
(provider-side, promise-valued so streaming settles ride the payload)
instead of tying transfer to a rendered node's positional id. Client
hooks look their own hash up and prime through query-core hydrate();
the registry outlives the hydration window, so prefetched-never-rendered
queries transfer and late mounts (lazy routes, post-hydration navigation)
adopt the server payload instead of refetching — restoring full
cache-level dehydration coverage with less machinery. Priming moved ahead
of the attach render effect (its effect half runs synchronously inside an
active flush, and mount policy against a cold cache would refetch).
Drops the peekNextChildId dependency; the data root reverts to { value }.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
…ck#8950 deprecation

The create*/Create* names were kept as a migration bridge when v5 made
use* canonical (TanStack#8950 deprecated them). This major already rewrites every
callsite's semantics, so the bridge retires: use* is the only naming,
matching the docs.

Co-authored-by: Cursor <cursoragent@cursor.com>
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