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

coderabbitaiBot 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

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
ryansolidforce-pushed the feat/solid-native-ssr-transfer branch from af88d3d to d30f736CompareSeptember 2, 2026 20:45
ryansolidand 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
ryansolidforce-pushed the feat/solid-native-ssr-transfer branch 2 times, most recently from d8fed87 to 51f9c95CompareSeptember 2, 2026 21:23
@ryansolid
ryansolid marked this pull request as ready for review September 2, 2026 21:24
@nx-cloud

nx-cloudBot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

View your CI Pipeline Execution ↗ for commit 78b9028

CommandStatusDurationResult
nx affected --targets=test:eslint,test:unit,tes...✅ Succeeded6m 57sView ↗
nx run-many --target=build --exclude=examples/*...✅ Succeeded51sView ↗

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

@pkg-pr-new

pkg-pr-newBot 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

ryansolidand 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
ryansolidforce-pushed the feat/solid-native-ssr-transfer branch from 51f9c95 to fab8acbCompareSeptember 2, 2026 22:15
@codspeed-hq

codspeed-hqBot 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-cloudnx-cloudBot 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-preSep 2, 2026
23 of 25 checks passed
@github-actionsgithub-actionsBot mentioned this pull request Sep 2, 2026
Sign up for freeto 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

@ryansolid@Sheraff@brenelz
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
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

coderabbitaiBot 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

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
ryansolidforce-pushed the feat/solid-native-ssr-transfer branch from af88d3d to d30f736CompareSeptember 2, 2026 20:45
ryansolidand 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
ryansolidforce-pushed the feat/solid-native-ssr-transfer branch 2 times, most recently from d8fed87 to 51f9c95CompareSeptember 2, 2026 21:23
@ryansolid
ryansolid marked this pull request as ready for review September 2, 2026 21:24
@nx-cloud

nx-cloudBot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

View your CI Pipeline Execution ↗ for commit 78b9028

CommandStatusDurationResult
nx affected --targets=test:eslint,test:unit,tes...✅ Succeeded6m 57sView ↗
nx run-many --target=build --exclude=examples/*...✅ Succeeded51sView ↗

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

@pkg-pr-new

pkg-pr-newBot 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

ryansolidand 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
ryansolidforce-pushed the feat/solid-native-ssr-transfer branch from 51f9c95 to fab8acbCompareSeptember 2, 2026 22:15
@codspeed-hq

codspeed-hqBot 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-cloudnx-cloudBot 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-preSep 2, 2026
23 of 25 checks passed
@github-actionsgithub-actionsBot mentioned this pull request Sep 2, 2026
Sign up for freeto 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

@ryansolid@Sheraff@brenelz
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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

coderabbitaiBot 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

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
ryansolidforce-pushed the feat/solid-native-ssr-transfer branch from af88d3d to d30f736CompareSeptember 2, 2026 20:45
ryansolidand 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
ryansolidforce-pushed the feat/solid-native-ssr-transfer branch 2 times, most recently from d8fed87 to 51f9c95CompareSeptember 2, 2026 21:23
@ryansolid
ryansolid marked this pull request as ready for review September 2, 2026 21:24
@nx-cloud

nx-cloudBot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

View your CI Pipeline Execution ↗ for commit 78b9028

CommandStatusDurationResult
nx affected --targets=test:eslint,test:unit,tes...✅ Succeeded6m 57sView ↗
nx run-many --target=build --exclude=examples/*...✅ Succeeded51sView ↗

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

@pkg-pr-new

pkg-pr-newBot 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

ryansolidand 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
ryansolidforce-pushed the feat/solid-native-ssr-transfer branch from 51f9c95 to fab8acbCompareSeptember 2, 2026 22:15
@codspeed-hq

codspeed-hqBot 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-cloudnx-cloudBot 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-preSep 2, 2026
23 of 25 checks passed
@github-actionsgithub-actionsBot mentioned this pull request Sep 2, 2026
Sign up for freeto 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

@ryansolid@Sheraff@brenelz
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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

coderabbitaiBot 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

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
ryansolidforce-pushed the feat/solid-native-ssr-transfer branch from af88d3d to d30f736CompareSeptember 2, 2026 20:45
ryansolidand 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
ryansolidforce-pushed the feat/solid-native-ssr-transfer branch 2 times, most recently from d8fed87 to 51f9c95CompareSeptember 2, 2026 21:23
@ryansolid
ryansolid marked this pull request as ready for review September 2, 2026 21:24
@nx-cloud

nx-cloudBot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

View your CI Pipeline Execution ↗ for commit 78b9028

CommandStatusDurationResult
nx affected --targets=test:eslint,test:unit,tes...✅ Succeeded6m 57sView ↗
nx run-many --target=build --exclude=examples/*...✅ Succeeded51sView ↗

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

@pkg-pr-new

pkg-pr-newBot 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

ryansolidand 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
ryansolidforce-pushed the feat/solid-native-ssr-transfer branch from 51f9c95 to fab8acbCompareSeptember 2, 2026 22:15
@codspeed-hq

codspeed-hqBot 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-cloudnx-cloudBot 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-preSep 2, 2026
23 of 25 checks passed
@github-actionsgithub-actionsBot mentioned this pull request Sep 2, 2026
Sign up for freeto 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

@ryansolid@Sheraff@brenelz
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
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

coderabbitaiBot 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

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
ryansolidforce-pushed the feat/solid-native-ssr-transfer branch from af88d3d to d30f736CompareSeptember 2, 2026 20:45
ryansolidand 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
ryansolidforce-pushed the feat/solid-native-ssr-transfer branch 2 times, most recently from d8fed87 to 51f9c95CompareSeptember 2, 2026 21:23
@ryansolid
ryansolid marked this pull request as ready for review September 2, 2026 21:24
@nx-cloud

nx-cloudBot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

View your CI Pipeline Execution ↗ for commit 78b9028

CommandStatusDurationResult
nx affected --targets=test:eslint,test:unit,tes...✅ Succeeded6m 57sView ↗
nx run-many --target=build --exclude=examples/*...✅ Succeeded51sView ↗

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

@pkg-pr-new

pkg-pr-newBot 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

ryansolidand 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
ryansolidforce-pushed the feat/solid-native-ssr-transfer branch from 51f9c95 to fab8acbCompareSeptember 2, 2026 22:15
@codspeed-hq

codspeed-hqBot 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-cloudnx-cloudBot 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-preSep 2, 2026
23 of 25 checks passed
@github-actionsgithub-actionsBot mentioned this pull request Sep 2, 2026
Sign up for freeto 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

@ryansolid@Sheraff@brenelz
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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

coderabbitaiBot 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

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
ryansolidforce-pushed the feat/solid-native-ssr-transfer branch from af88d3d to d30f736CompareSeptember 2, 2026 20:45
ryansolidand 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
ryansolidforce-pushed the feat/solid-native-ssr-transfer branch 2 times, most recently from d8fed87 to 51f9c95CompareSeptember 2, 2026 21:23
@ryansolid
ryansolid marked this pull request as ready for review September 2, 2026 21:24
@nx-cloud

nx-cloudBot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

View your CI Pipeline Execution ↗ for commit 78b9028

CommandStatusDurationResult
nx affected --targets=test:eslint,test:unit,tes...✅ Succeeded6m 57sView ↗
nx run-many --target=build --exclude=examples/*...✅ Succeeded51sView ↗

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

@pkg-pr-new

pkg-pr-newBot 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

ryansolidand 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
ryansolidforce-pushed the feat/solid-native-ssr-transfer branch from 51f9c95 to fab8acbCompareSeptember 2, 2026 22:15
@codspeed-hq

codspeed-hqBot 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-cloudnx-cloudBot 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-preSep 2, 2026
23 of 25 checks passed
@github-actionsgithub-actionsBot mentioned this pull request Sep 2, 2026
Sign up for freeto 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

@ryansolid@Sheraff@brenelz
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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

coderabbitaiBot 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

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
ryansolidforce-pushed the feat/solid-native-ssr-transfer branch from af88d3d to d30f736CompareSeptember 2, 2026 20:45
ryansolidand 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
ryansolidforce-pushed the feat/solid-native-ssr-transfer branch 2 times, most recently from d8fed87 to 51f9c95CompareSeptember 2, 2026 21:23
@ryansolid
ryansolid marked this pull request as ready for review September 2, 2026 21:24
@nx-cloud

nx-cloudBot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

View your CI Pipeline Execution ↗ for commit 78b9028

CommandStatusDurationResult
nx affected --targets=test:eslint,test:unit,tes...✅ Succeeded6m 57sView ↗
nx run-many --target=build --exclude=examples/*...✅ Succeeded51sView ↗

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

@pkg-pr-new

pkg-pr-newBot 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

ryansolidand 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
ryansolidforce-pushed the feat/solid-native-ssr-transfer branch from 51f9c95 to fab8acbCompareSeptember 2, 2026 22:15
@codspeed-hq

codspeed-hqBot 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-cloudnx-cloudBot 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-preSep 2, 2026
23 of 25 checks passed
@github-actionsgithub-actionsBot mentioned this pull request Sep 2, 2026
Sign up for freeto 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

@ryansolid@Sheraff@brenelz
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
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

coderabbitaiBot 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

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
ryansolidforce-pushed the feat/solid-native-ssr-transfer branch from af88d3d to d30f736CompareSeptember 2, 2026 20:45
ryansolidand 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
ryansolidforce-pushed the feat/solid-native-ssr-transfer branch 2 times, most recently from d8fed87 to 51f9c95CompareSeptember 2, 2026 21:23
@ryansolid
ryansolid marked this pull request as ready for review September 2, 2026 21:24
@nx-cloud

nx-cloudBot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

View your CI Pipeline Execution ↗ for commit 78b9028

CommandStatusDurationResult
nx affected --targets=test:eslint,test:unit,tes...✅ Succeeded6m 57sView ↗
nx run-many --target=build --exclude=examples/*...✅ Succeeded51sView ↗

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

@pkg-pr-new

pkg-pr-newBot 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

ryansolidand 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
ryansolidforce-pushed the feat/solid-native-ssr-transfer branch from 51f9c95 to fab8acbCompareSeptember 2, 2026 22:15
@codspeed-hq

codspeed-hqBot 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-cloudnx-cloudBot 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-preSep 2, 2026
23 of 25 checks passed
@github-actionsgithub-actionsBot mentioned this pull request Sep 2, 2026
Sign up for freeto 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

@ryansolid@Sheraff@brenelz