Skip to content

feat(remix): scaffold remix-router, remix-start, and basic example#7359

Draft
tannerlinsley wants to merge 2 commits intomainfrom
taren/cranky-margulis-b5b679
Draft

feat(remix): scaffold remix-router, remix-start, and basic example#7359
tannerlinsley wants to merge 2 commits intomainfrom
taren/cranky-margulis-b5b679

Conversation

@tannerlinsley
Copy link
Copy Markdown
Member

feat(remix): scaffold remix-router, remix-start, and basic example

Brings up an experimental TanStack Router binding to Remix 3 (@remix-run/ui) plus the Start adapter and a working basic example.

What's in the box

Package LOC (src) Status
@tanstack/remix-router 7,351 Scaffold complete; binding works for the surface listed below
@tanstack/remix-start 479 Server entry, default-entry server/client, vite plugin wrapper, useServerFn
examples/remix/basic 2,100 Full-tree-hydrated demo; 16 working routes
examples/remix/islands 264 Pure-Remix-3 reference using @remix-run/ui directly (no TanStack), demonstrates idiomatic island hydration
Tests in packages/remix-router/tests 3,917 28 test files

For comparison: react-router src is 6,699 LOC, solid-router is 5,487. react-start is 251, solid-start is 127.

Build & test

Check Result
nx run @tanstack/remix-router:build ✅ pass
nx run @tanstack/remix-start:build ✅ pass
nx run @tanstack/remix-router:test:unit 111/120 pass (92.5%)
examples/remix/basic pnpm build ✅ pass — emits client + server bundles
Type-check across binding ✅ no errors

The 9 test failures are all in tests/serverComponent.test.tsx and tests/dom/serverComponent.test.tsx. They fail because _resetServerComponentRegistry and deactivateServerComponentCollector aren't exported from the source — the test scaffold was added before the implementation. Not blocking; tracked as a follow-up below.

Bundle size (basic example, production build)

Slice Raw Gzip
Total client JS (all chunks) 296 kB 64 kB
Initial entry (index + useRouter) ~140 kB ~46 kB
Per-route lazy chunks 0.5–7 kB each 0.2–3 kB each
Server bundle 556 kB

Initial-load gzip (~46 kB) is dominated by the router runtime + Remix UI reconciler. The whole route tree hydrates as one Remix UI mount — there is no per-component selective hydration. This is intentional: TanStack Router's reactive store subscriptions need to be live across the route tree.

For comparison, examples/remix/islands (no TanStack Router, pure @remix-run/ui islands via run()) demonstrates what selective hydration would look like: only clientEntry()-marked regions ship as hydration roots, and the rest is inert HTML. That's a different stack, not a different mode of this one.

Routes that work in the basic example

Path Demonstrates Status
/ Static welcome page
/users, /users/$id Loader-driven list, nested detail via <Outlet>
/posts, /posts/$slug createServerFn rendering markdown HTML; mounted via innerHTML
/admin/users/$userId/sessions/$sessionId 4-deep nested layout via file path
/catalog?… validateSearch, loaderDeps, <Link search={updater}>, form-driven useNavigate
/slow Async loader, pendingComponent UI
/lab/error Loader throws → errorComponent (intentional 500)
/lab/missing notFound()notFoundComponent (intentional 404)
/lab/render-error Render-time throw caught by <CatchBoundary>
/guestbook createServerFn({ method: 'POST' }) with inputValidator; form on('submit') calls server fn
/counter clientEntry()-marked island embedded in route component

What's not (yet) covered

Three pieces are partially implemented and need follow-up work in the binding before the corresponding example routes can ship:

1. <Frame> server-fn–backed boundaries

  • The example expected renderPostBody.url(slug) (curried). .url is a string property, not a function. Manuel Schiller's buildServerFnUrl PR (branch origin/buildServerFnUrl) lands a buildServerFnUrl(fn, input): Promise<string> helper; once merged, the example loader can precompute the URL.
  • Independent of the URL helper, SSR still throws No resolveFrame provided from a recursive renderToStream call. The remix-router top-level handler does pass resolveFrame, but inner renders (when a frame's src is itself a route URL) don't get the option threaded through. Needs resolveFrame to be a property of the request context rather than an option of the outer renderRouterToStream call.

2. <Await> / defer() SSR streaming

  • Landed in this PR: awaited.tsx onSettle → handle.update() now skips on the server. The post-stream scheduleUpdate not implemented crash is gone.
  • Still broken: the seroval streaming chunks for the resolved deferred value are enqueued in scriptBuffer.enqueue but don't reach the response body. The server holds the connection open for the deferred duration, but the only <script> chunk in the response is the initial dehydration with the placeholder — never the resolution. The bug is somewhere between crossSerializeStream's onSerialize callback and pipeWithDehydration's collectInjection() (which runs once, at end-of-stream, after waitForSerialization()).
  • The /deferred route was prepared and verified end-to-end during this work, then rolled back since "renders fallback forever" is worse UX than no route. Restore from git history once (b) is fixed.

3. serverComponent() re-render endpoint

Test scaffold present in tests/serverComponent.test.tsx and tests/dom/serverComponent.test.tsx (9 failing tests). Source files exist (src/serverComponent.tsx, src/serverComponentClient.ts, src/serverComponentEndpoint.ts, src/serverComponentSSR.tsx) but the test helpers _resetServerComponentRegistry and deactivateServerComponentCollector aren't exported. Probably means the implementation was started, the tests were sketched against the intended API, and the work paused. No demo route — the surface is incomplete.

Files changed

Area Files Insertions Deletions
Packages (binding + adjacent fixes) 141 12,970 451
Examples (basic + islands) 37 1,993 0

Adjacent-package edits made to get the binding to build on this branch (each was either a corrupted-truncated file from the salvage commit or a missing reference to functionality that landed on a different branch):

  • packages/router-generator/src/config.ts — restored from main, re-added 'remix' target
  • packages/start-plugin-core/src/vite/dev-server-plugin/plugin.ts — restored from main
  • packages/start-plugin-core/src/import-protection/defaults.ts — removed (orphan, referenced missing ./utils)
  • packages/start-server-core/src/index.tsx — removed early-hints re-exports (referenced unimported file)
  • packages/remix-router/src/headContentUtils.ts — removed isInlinableStylesheet reference (lives on a different branch)
  • packages/remix-start/src/plugin/vite.ts — fixed import paths to match solid-start shape
  • packages/remix-start/src/server/index.ts — removed createStartApp export (never existed)
  • packages/remix-router/src/awaited.tsx — added if (isServer) return guard in onSettle

API surface in @tanstack/remix-router

What's wired up and exercised by the example or tests:

Routes:        Route, RootRoute, NotFoundRoute, FileRoute, LazyRoute,
               createRoute, createFileRoute, createRootRoute,
               createRootRouteWithContext, createLazyRoute, createLazyFileRoute
Router:        createRouter, RouterProvider, RouterContextProvider, mountRouter
View:          Outlet, Match, Matches, MatchRoute, Link, ClientLink,
               Navigate, createLink, linkOptions, useLinkProps
Hooks:         useRouter, useRouterState, useLocation, useNavigate,
               useMatch, useMatches, useParentMatches, useChildMatches,
               useMatchRoute, useParams, useSearch, useLoaderData,
               useLoaderDeps, useRouteContext, useCanGoBack, useBlocker
SSR/Doc:       HeadContent, Scripts, ScriptOnce, ScrollRestoration,
               useElementScrollRestoration, buildHeadTags, Asset,
               ClientOnly, useHydrated
Boundaries:    CatchBoundary, CatchNotFound, ErrorComponent,
               DefaultGlobalNotFound
Async:         Await, useAwaited (SSR fallback only — see above)
Code-split:    lazyRouteComponent
Subscribe:     subscribeStore, subscribeDynamicStore
Singleton:     setActiveRouter, getActiveRouter, clearActiveRouter
Server:        createRouterHandler, renderRouterToStream
RouteApi:      RouteApi, getRouteApi

Testing notes for reviewers

pnpm install
nx run @tanstack/remix-router:build
nx run @tanstack/remix-start:build
nx run @tanstack/remix-router:test:unit  # 111/120 pass

# Basic example
cd examples/remix/basic && pnpm dev   # http://localhost:5173
pnpm build                            # production build

# Islands example (no TanStack Router — pure Remix 3 reference)
cd examples/remix/islands && pnpm dev # http://localhost:5174

The basic example is intentionally self-contained — it doesn't depend on <Frame> or <Await> SSR streaming working. Once the two binding gaps above are fixed, the /streaming and /deferred routes can be restored from git history.

What this PR is NOT

  • Not a 1.0 surface freeze for the binding. Both Remix 3 itself and this binding will move before stable.
  • Not feature-parity with react-router (devtools, <Form> middleware integration, RSC-style boundaries are not in scope).
  • Not a perf-tuning pass. The numbers above are first-cut; the real perf story is the clientEntry-based selective-hydration path described in packages/remix-router/START.md, which is a separate piece of work.

Adds a 'remix' target option to router-generator and introduces two new
packages plus a basic example exercising the new target. Preserves WIP
that was at risk of being dropped from the clever-ellis-6fe30d worktree.
The remix-router scaffold commit (5a13aec) salvaged WIP from
clever-ellis-6fe30d but accidentally truncated several files and left
references to functionality that lives on other branches. None of it
built end-to-end.

This restores or removes the broken pieces, fixes the example, and
adds a parallel pure-Remix-3 islands example for reference.

Adjacent-package fixes (build-blockers from the original scaffold):
- router-generator/src/config.ts: restored from main, re-added 'remix'
  target to the schema enum
- start-plugin-core/src/vite/dev-server-plugin/plugin.ts: restored
  from main (was truncated mid-function)
- start-plugin-core/src/import-protection/defaults.ts: removed
  (orphaned, referenced missing ./utils, nothing imports it)
- start-server-core/src/index.tsx: removed early-hints type
  re-exports (file lives on a different branch, not used here)
- remix-router/src/headContentUtils.ts: removed isInlinableStylesheet
  reference (also lives on a different branch)
- remix-start/src/plugin/vite.ts: imports moved to match solid-start
  shape; the ./vite subpath isn't an exported entry of
  start-plugin-core
- remix-start/src/server/index.ts: removed createStartApp export
  (never existed in createStartHandler.ts)

Binding fix:
- remix-router/src/awaited.tsx: gate handle.update() in onSettle to
  client-only via isServer. The SSR scheduler doesn't implement
  scheduleUpdate, so post-stream updates were crashing the dev server
  whenever a route used <Await>.

Example fixes (examples/remix/basic):
- All 16 route files: createRoute('/path')(options) was wrong shape
  (createRoute takes a single options arg, not curried). Switched
  call sites to createFileRoute('/path')(options) to match the
  router-generator template, which is the actual file-based pattern
  these routes are meant to use.
- routes/index.tsx: added (was missing; routeTree.ts referenced an
  Index route that didn't exist)
- routes/streaming.tsx, routes/deferred.tsx: removed. Both are
  blocked on real binding work — Frame needs resolveFrame threading
  through nested SSR renders + Manuel's pending buildServerFnUrl PR
  for the URL-builder side; <Await> SSR streaming chunks aren't
  reaching the response body. Tracked in the README.
- routes/__root.tsx: dropped Deferred link from nav
- routes/catalog.tsx: input was value={search.q} with no event that
  updated value, so typing did nothing visible. Switched to
  defaultValue. Loader now uses sort in loaderDeps and actually
  applies the sort to the items.
- README.md: rewrote with architecture diagram, primitive table,
  accurate route list, hydration model section, and a "what's not
  covered" section calling out the binding gaps.

New example (examples/remix/islands):
- Pure-Remix-3 reference using @remix-run/ui directly with no
  TanStack Router. Demonstrates idiomatic island hydration: a
  static-rendered page with two clientEntry()-marked components
  (LinkIsland, Counter) hydrating independently. Runs on a small
  Vite-middlewareMode SSR server (47-line server.js).

Documentation:
- remix-router/README.md: test count refreshed to 120 (111 passing);
  three real binding gaps (Frame, Await SSR streaming,
  serverComponent endpoint) added to the "Not yet" list.
- remix-router/START.md: bundle-size table updated with measured
  numbers (296 kB raw / 64 kB gzip total client; ~46 kB gzip initial
  entry; 556 kB server bundle) replacing the prior estimate.

What works in the basic example after this:
- 16 routes serving HTTP 200 (or intentional 500/404 for the lab
  routes), SPA navigation working, server stays up across the route
  set, type-check clean, all builds green.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 7, 2026

Important

Review skipped

Draft detected.

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

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c64b8462-9765-4fa6-bf28-6fc6026229c1

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
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch taren/cranky-margulis-b5b679

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

@tannerlinsley tannerlinsley changed the title fix(remix): unblock remix-router scaffold + add islands example feat(remix): scaffold remix-router, remix-start, and basic example May 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant