Skip to content

feat(solid-router): native SSR match transfer, hydration-claiming boot, provider-owned dispatch - #8213

Merged
brenelz merged 14 commits into
TanStack:solid-router-v2-prefrom
ryansolid:feat/solid-native-ssr-transfer
Sep 2, 2026
Merged

feat(solid-router): native SSR match transfer, hydration-claiming boot, provider-owned dispatch#8213
brenelz merged 14 commits into
TanStack:solid-router-v2-prefrom
ryansolid:feat/solid-native-ssr-transfer

Conversation

@ryansolid

Copy link
Copy Markdown

Summary

Phase 1 of the Solid-native SSR plan (RFC included in the diff: RFC-solid-native-ssr.md). Replaces the React-inherited SSR bootstrapping in the Solid adapter with Solid's own hydration machinery:

  • Registry match transfer: the server serializes router match state (loaderData, status, __beforeLoadContext) into Solid's hydration registry under tsr: keys — the same channel solid-query uses — instead of relying on the $_TSR script for match data. Deferred loaderData fields stream natively through seroval promise serialization.
  • Hydration-claiming boot: the client primes the router from the registry in the Router constructor, before hydrate() runs. Matches are committed outside the hydration render's owned scope, so downstream apps no longer need boot-time router.load() workarounds (e.g. the bootLoad pattern) or prefetch pausing.
  • Provider-owned server dispatch: RouterProvider kicks off router.load() on the server and parks the render on it via an async memo, preserving blocking loader semantics through Solid's async SSR (renderToStream) with no explicit await router.load() in entry files.

Includes a protocol-less external-SSR repro harness (packages/solid-router/repro-external-ssr/) that round-trips server render → hydration in jsdom and asserts loader single-run, node claiming, deferred streaming order, and interactivity.

Stacked on #8192 (named single-flight sources) — review the last 4 commits.

Test plan

  • @tanstack/solid-router unit + server-mode suites
  • External-SSR repro harness (streamed chunks, deferred loaderData, zero loader re-runs after hydrate)
  • Solid Start e2e suites (basic, server-functions, basic-solid-query among others)

Made with Cursor

…els carry the Router + Query pairing

solid-query v6's QueryClientProvider serializes the request's cache into
Solid's hydration registry during SSR and primes the client cache from
it, so running the ssr-query transport alongside it ships every query
payload twice. The package's two runtime conveniences are each a few
lines of userland composition on public APIs: the provider wrap via the
router's Wrap option, and cache-driven redirect() errors handed to
router.navigate from the caches' config.onError.

Converts the three Solid Start e2e apps to the composition (all suites
green, including the redirect-from-query tests) and marks the package
deprecated for the v2 line.

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

coderabbitai Bot commented Sep 2, 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: Team

Run ID: b48f7941-d856-43db-ab61-5cd162f0ac56

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.

@Sheraff

Sheraff commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

just a small FYI: we try to use isServer ?? router.isServer instead of just isServer so that in development mode, router.isServer can define the value (in production, isServer is always defined true or false, so this gets dead-code eliminated anyway)

This helps people who instantiate a router in their unit tests

@ryansolid
ryansolid force-pushed the feat/solid-native-ssr-transfer branch from af88d3d to d30f736 Compare September 2, 2026 20:45
ryansolid and others added 5 commits September 2, 2026 14:14
Co-authored-by: Cursor <cursoragent@cursor.com>
Solid's single-flight channel is becoming multi-source (solidjs/solid
653dd41e): mutation responses carry a keyed envelope of per-cache
slices, each routed to the consumer subscribed under its source id.
Today Start claims the single unnamed slot on both halves, which means
any other cache wanting mutation-response data (e.g. solid-query, whose
provider subscribes under "sq" in TanStack/query#11326) displaces the
router's — whichever registers last wins, silently.

The router's flight data now rides its own source id ("tsr"): the
server collector registers additively with registerFlightDataSource and
the client subscribes its consumer under the same id, so router
loader/match state and other caches' slices coexist on one round trip.
A user-supplied collectFlightData hook keeps the unnamed slot to itself,
adding data alongside the router's instead of displacing it.

Both halves feature-detect the protocol on the installed @solidjs/web
(it ships in the release after 2.0.0-rc.4) and fall back to the exact
previous unnamed-slot behavior on older versions; since client and
server resolve the same install, the halves cannot disagree.

Co-authored-by: Cursor <cursoragent@cursor.com>
…t trigger

The router's half of flight collection as a public primitive: derive the
flight request for the mutation's target, run the matched routes' data
functions, hand the loaded router to the caller's collect() — any cache
(the router's own state, a query client) composes its extraction on top.
Start's collector now consumes it; errors are contained per Solid
Router's collector convention (flight data is an optimization, never a
mutation error).

Co-authored-by: Cursor <cursoragent@cursor.com>
…5 fallback

@solidjs/web 2.0.0-rc.5 ships the multi-source single-flight protocol, so
the feature detection and cast shims bridging unreleased types come out:
the client subscribes directly under SOLID_START_FLIGHT_SOURCE, the server
registers its collector via registerFlightDataSource unconditionally (the
unnamed collectFlightData slot now always belongs to the user), and the
@solidjs/web peer floor moves to rc.5.

Co-authored-by: Cursor <cursoragent@cursor.com>
With the pre-rc.5 fallback stripped, the client advertises its named
source and the server echoes what it folded: X-Single-Flight is "tsr" on
both sides, not the legacy "true".

Co-authored-by: Cursor <cursoragent@cursor.com>
@ryansolid
ryansolid force-pushed the feat/solid-native-ssr-transfer branch 2 times, most recently from d8fed87 to 51f9c95 Compare September 2, 2026 21:23
@ryansolid
ryansolid marked this pull request as ready for review September 2, 2026 21:24
@nx-cloud

nx-cloud Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

View your CI Pipeline Execution ↗ for commit 78b9028

Command Status Duration Result
nx affected --targets=test:eslint,test:unit,tes... ✅ Succeeded 6m 57s View ↗
nx run-many --target=build --exclude=examples/*... ✅ Succeeded 51s View ↗

☁️ Nx Cloud last updated this comment at 2026-09-02 23:43:09 UTC

@pkg-pr-new

pkg-pr-new Bot commented Sep 2, 2026

Copy link
Copy Markdown
More templates

@tanstack/arktype-adapter

npm i https://pkg.pr.new/@tanstack/arktype-adapter@8213

@tanstack/eslint-plugin-router

npm i https://pkg.pr.new/@tanstack/eslint-plugin-router@8213

@tanstack/eslint-plugin-start

npm i https://pkg.pr.new/@tanstack/eslint-plugin-start@8213

@tanstack/history

npm i https://pkg.pr.new/@tanstack/history@8213

@tanstack/nitro-v2-vite-plugin

npm i https://pkg.pr.new/@tanstack/nitro-v2-vite-plugin@8213

@tanstack/react-router

npm i https://pkg.pr.new/@tanstack/react-router@8213

@tanstack/react-router-devtools

npm i https://pkg.pr.new/@tanstack/react-router-devtools@8213

@tanstack/react-router-ssr-query

npm i https://pkg.pr.new/@tanstack/react-router-ssr-query@8213

@tanstack/react-start

npm i https://pkg.pr.new/@tanstack/react-start@8213

@tanstack/react-start-client

npm i https://pkg.pr.new/@tanstack/react-start-client@8213

@tanstack/react-start-rsc

npm i https://pkg.pr.new/@tanstack/react-start-rsc@8213

@tanstack/react-start-server

npm i https://pkg.pr.new/@tanstack/react-start-server@8213

@tanstack/router-cli

npm i https://pkg.pr.new/@tanstack/router-cli@8213

@tanstack/router-core

npm i https://pkg.pr.new/@tanstack/router-core@8213

@tanstack/router-devtools

npm i https://pkg.pr.new/@tanstack/router-devtools@8213

@tanstack/router-devtools-core

npm i https://pkg.pr.new/@tanstack/router-devtools-core@8213

@tanstack/router-generator

npm i https://pkg.pr.new/@tanstack/router-generator@8213

@tanstack/router-plugin

npm i https://pkg.pr.new/@tanstack/router-plugin@8213

@tanstack/router-ssr-query-core

npm i https://pkg.pr.new/@tanstack/router-ssr-query-core@8213

@tanstack/router-utils

npm i https://pkg.pr.new/@tanstack/router-utils@8213

@tanstack/router-vite-plugin

npm i https://pkg.pr.new/@tanstack/router-vite-plugin@8213

@tanstack/solid-router

npm i https://pkg.pr.new/@tanstack/solid-router@8213

@tanstack/solid-router-devtools

npm i https://pkg.pr.new/@tanstack/solid-router-devtools@8213

@tanstack/solid-router-ssr-query

npm i https://pkg.pr.new/@tanstack/solid-router-ssr-query@8213

@tanstack/solid-start

npm i https://pkg.pr.new/@tanstack/solid-start@8213

@tanstack/solid-start-client

npm i https://pkg.pr.new/@tanstack/solid-start-client@8213

@tanstack/solid-start-server

npm i https://pkg.pr.new/@tanstack/solid-start-server@8213

@tanstack/start-client-core

npm i https://pkg.pr.new/@tanstack/start-client-core@8213

@tanstack/start-fn-stubs

npm i https://pkg.pr.new/@tanstack/start-fn-stubs@8213

@tanstack/start-plugin-core

npm i https://pkg.pr.new/@tanstack/start-plugin-core@8213

@tanstack/start-server-core

npm i https://pkg.pr.new/@tanstack/start-server-core@8213

@tanstack/start-static-server-functions

npm i https://pkg.pr.new/@tanstack/start-static-server-functions@8213

@tanstack/start-storage-context

npm i https://pkg.pr.new/@tanstack/start-storage-context@8213

@tanstack/valibot-adapter

npm i https://pkg.pr.new/@tanstack/valibot-adapter@8213

@tanstack/virtual-file-routes

npm i https://pkg.pr.new/@tanstack/virtual-file-routes@8213

@tanstack/vue-router

npm i https://pkg.pr.new/@tanstack/vue-router@8213

@tanstack/vue-router-devtools

npm i https://pkg.pr.new/@tanstack/vue-router-devtools@8213

@tanstack/vue-router-ssr-query

npm i https://pkg.pr.new/@tanstack/vue-router-ssr-query@8213

@tanstack/vue-start

npm i https://pkg.pr.new/@tanstack/vue-start@8213

@tanstack/vue-start-client

npm i https://pkg.pr.new/@tanstack/vue-start-client@8213

@tanstack/vue-start-server

npm i https://pkg.pr.new/@tanstack/vue-start-server@8213

@tanstack/zod-adapter

npm i https://pkg.pr.new/@tanstack/zod-adapter@8213

commit: 59e1224

ryansolid and others added 6 commits September 2, 2026 15:15
…rc.6

Repo-wide (packages, examples, e2e apps, benchmarks) — a scoped bump
leaves the workspace mixed, and examples/benchmarks then build workspace
solid-start dists (which import registerFlightDataSource, rc.5+) against
their own @solidjs/web rc.4 resolution. rc.6 ships the named flight-data
source API this branch requires plus the settle-walk fix that unblocks
the Solid Query pairing. @tanstack/solid-start's peer floor moves to
rc.6.

The SSR bench helpers move onto rc.6's wire shape: scripted callers use
the data address (`<endpoint>/data/<id>`) — the bare address now answers
document traffic with the no-JS convention.

Co-authored-by: Cursor <cursoragent@cursor.com>
…aiming boot

Proves the RFC's Phase 1 claims on the external-SSR harness, against
published core (web 2.0.0-rc.5):

- Server render serializes each match's loaderData/status into Solid's
  hydration registry, content-addressed (`tsr:<matchId>`), the identical
  mechanism solid-query v6 ships queries through — no `__TSR_SSR__`
  script channel.
- The client boot matches synchronously, primes match state from the
  registry (populated at document parse), and commits — no
  `router.load()` before hydrate, loaders do not re-run (0 client runs),
  hydration claims the server DOM identically, and post-hydration
  navigation with an unresolved chunk still shows pending UI and settles
  under its boundary.

Spike-level notes: the commit must happen before hydrate() (store writes
inside the hydration render are owned-scope writes), the transfer covers
settled matches (promise-valued entries for pending loaders are the same
serialize call, next step), and the serialization-context guard keys on
`ctx.serialize` presence (`ctx.async` is not set under this web
version's renderToString).

Co-authored-by: Cursor <cursoragent@cursor.com>
…ot (Phase 1)

Grows the spike into the adapter. RouterProvider serializes each settled
match's state (loaderData, status, error, notFound, beforeLoadContext,
ssr) into Solid's hydration registry during server render —
content-addressed (`tsr:<matchId>`), the same channel solid-query v6
ships queries through, no `__TSR_SSR__` script injection. The Router
constructor owns the client half: when the registry holds entries for the
synchronously matched routes, it primes and commits match state at
creation — always outside a render, after the document (and therefore the
entries) parsed, before hydrate(). No load pass before hydration, no
loader re-runs; route chunks resolve at the read point under the
boundaries the server rendered.

Both halves are inert outside the bare pairing: the server skips when
`router.serverSsr` marks the Start contract, and the boot falls through
on the first missing entry (SPA pages, Start's own channel).

Placement is load-bearing: committing inside the hydration render — even
with writes moved off the owner — leaves the claiming walk's registry
bookkeeping desynced (nodes reuse correctly but audit as unclaimed).
Router creation is the client's natural pre-render moment, and the
harness A/B proved it clean.

Validated: external-SSR harness green end to end (registry primed, zero
client loader runs, identical DOM claimed, pending UI on post-hydration
nav), solid-router unit suite 865 tests green, and all 37 Solid Start e2e
tests green against the rc.6-candidate core (workspace tarballs) with the
named-source strips in place. The harness now contains zero transfer
code. Pending matches are skipped, not deferred — promise-valued entries
(streaming SSR) are the next increment.

Co-authored-by: Cursor <cursoragent@cursor.com>
…er, boundary bullet corrected

Co-authored-by: Cursor <cursoragent@cursor.com>
RouterProvider now owns router.load() on the server, parking the render
on it through an async memo — no more explicit await router.load() in
entries; blocking semantics ride Solid's async SSR. The bare-pairing
harness moves to renderToStream and proves deferred loaderData promises
stream natively (fallback in the shell chunk, value in a later chunk,
settled through hydration without <Await>).

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@ryansolid
ryansolid force-pushed the feat/solid-native-ssr-transfer branch from 51f9c95 to fab8acb Compare September 2, 2026 22:15
@codspeed-hq

codspeed-hq Bot commented Sep 2, 2026

Copy link
Copy Markdown

Hooray! CodSpeed harness just leveled up!

The base and head of this comparison were measured with different runner settings, so their benchmark values are not directly comparable.

What changed between base and head:

Re-run the base with the same settings to get a valid performance comparison.


Comparing ryansolid:feat/solid-native-ssr-transfer (59e1224) with solid-router-v2-pre (67a9040)1

Open in CodSpeed

Footnotes

  1. No successful run was found on solid-router-v2-pre (96f50d9) during the generation of this report, so 67a9040 was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

nx-cloud[bot]

This comment was marked as outdated.

@nx-cloud nx-cloud 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.

Important

At least one additional CI pipeline execution has run since the conclusion below was written and it may no longer be applicable.

Nx Cloud is proposing a fix for your failed CI:

We fixed two classes of CI failure introduced by this PR: the new repro-external-ssr/ directory was added to packages/solid-router/tsconfig.json's include array so ESLint's TypeScript parser can resolve those files, and all createEffect(fn) single-argument calls across the affected example projects were migrated to createTrackedEffect(fn) — the solid-js 2.0 drop-in replacement for the old unified-callback form. These changes align the examples with the solid-js 2.0.0-rc.6 API that the PR bumped to.

Warning

  • We could not verify this fix.
  • The suggested diff is too large to display here, but you can view it on Nx Cloud ↗

Because this branch comes from a fork, it is not possible for us to apply fixes directly, but you can apply the changes locally using the available options below.

Apply changes locally with:

npx nx-cloud apply-locally MFnt-PGzv

Apply fix locally with your editor ↗   View interactive diff ↗



🎓 Learn more about Self-Healing CI on nx.dev

Two things were failing the Test job:

- @tanstack/solid-router test:eslint: the repro-external-ssr harness
  is not part of the package tsconfig, so the typed parser rejected
  its .tsx files. Ignore the harness in the package eslint config; it
  is a standalone vite script, not shipped code.

- Four example builds (basic-solid-query, basic-solid-query-file-based,
  kitchen-sink-file-based, kitchen-sink-solid-query) run tsc, and the
  rc.6 types no longer accept the one-argument createEffect form.
  Convert the eleven call sites to the two-argument
  createEffect(source, effect) form already used by the sibling
  examples and e2e apps.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@brenelz
brenelz merged commit b445b89 into TanStack:solid-router-v2-pre Sep 2, 2026
23 of 25 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.

3 participants