Skip to content

Add SPA routing with custom fetch router outputs - #11629

Open
brophdawg11 wants to merge 3 commits into
brophdawg11/rmx-replace-navigationfrom
brophdawg11/codex-spa-router
Open

Add SPA routing with custom fetch router outputs#11629
brophdawg11 wants to merge 3 commits into
brophdawg11/rmx-replace-navigationfrom
brophdawg11/codex-spa-router

Conversation

@brophdawg11

@brophdawg11 brophdawg11 commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

@remix-run/fetch-router currently assumes every dispatch produces a Response, even when the router is used entirely in memory. This adds a configurable output contract and a remix/ui/spa runtime for building browser-only applications whose routers map URLs directly to rendered UI.

  • Adds ambient RouterTypes.output configuration across routers, middleware, controllers, mounted routes, and nested context.router.fetch calls while retaining the existing string | URL | Request input contract.
  • Preserves an internal Request for every dispatch, including propagation from router.fetch(url, { signal }) to context.request.signal.
  • Strongly types response-mutating middleware as requiring Response output and throws at runtime if downstream middleware returns another value.
  • Exports SPA and the lower-level createSPA setup utility from remix/ui/spa. They intercept same-origin Navigation API navigations, cancel superseded loads, expose active and pending URLs, and respect rmx-document and rmx-history.
  • Preserves form methods and forwards non-GET FormData. By default, same-URL submissions replace history while different-URL submissions push; rmx-history can override this behavior. History traversals revisit form destinations with GET because navigation entries do not retain submitted data.
  • Adds a Vite demo in demos/spa showing route maps, controllers, render middleware, pending navigation UI, cancellation, and client-side form submissions.
import { createRouter } from 'remix/router'
import { createRoot, type RemixNode } from 'remix/ui'
import { SPA } from 'remix/ui/spa'

declare module 'remix/router' {
  interface RouterTypes {
    output: RemixNode
  }
}

let router = createRouter({
  defaultHandler: () => <h1>Not Found</h1>,
})

router.get('/', () => <h1>Home</h1>)
router.get('/about', () => <h1>About</h1>)

let root = createRoot(document.getElementById('app')!)
root.render(<SPA router={router} fallback="Loading…" />)

@github-actions

Copy link
Copy Markdown
Contributor

Preview Build Available

A preview build has been created for this PR. You can install it using:

pnpm install "remix-run/remix#preview/pr-11629&path:packages/remix"

This preview build will be updated automatically as you push new commits.

@brophdawg11 brophdawg11 self-assigned this Jul 23, 2026
actions: {
async home(context) {
await sleep(1000, context.request.signal)
return <HomePage />

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

actions now return Remix nodes

<code>context.request.formData()</code> without making an HTTP request. History traversals
return here with GET because navigation entries do not retain <code>FormData</code>.
</p>
<form method="POST" action={routes.greet.href()} mix={formStyle}>

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Form submissions are also handled by the SPA router component

}

export function Layout(handle: Handle<LayoutProps>) {
let router = handle.context.get(SPA)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Components can access active/pending URLs from the SPA context

Comment thread demos/spa/app/router.tsx
Comment on lines +8 to +12
declare module 'remix/router' {
interface RouterTypes {
output: RemixNode
}
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This is the important part to enable returning RemixNode's from the router

markmals added a commit to pitlane-tools/templates that referenced this pull request Jul 25, 2026
Switch from a Service Worker fetch handler to the client-side SPA router
from remix-run/remix#11629: the router declares RouterTypes.output =
RemixNode so handlers return JSX, and the SPA component from remix/ui/spa
dispatches navigations and form submissions through the in-memory router.

Frames stay: the guest book page is a <Frame name="welcome"> shell whose
src resolves through the same router via createRoot's frameInit, with the
frame name carried as the x-remix-target request header. Frames only
re-resolve when their src changes, so the POST action stamps a freshness
token into the src; the sign form is keyed by entry count so submissions
mount a fresh form instead of morphing old values.

The worker entry, Document shell, render middleware, and @pitlane/dev
plugin are gone — the build is a plain Vite SPA (dist + 404.html).

pnpm plumbing for the preview build: allowBuilds only matches git-hosted
packages by exact resolved commit, so both workspace files pin the remix
prepare (true) and decline the prebuilt @remix-run/* scripts (false);
blockExoticSubdeps is disabled for the preview's git-tarball dependency
graph, and the deploy workflow stops CI's frozen-lockfile default from
leaking into the git checkout prepared during install.
@brookslybrand
brookslybrand requested a review from mjackson August 3, 2026 19:18
@brophdawg11
brophdawg11 force-pushed the brophdawg11/codex-spa-router branch from 489d4d7 to 2f2c826 Compare August 6, 2026 16:07
@brophdawg11
brophdawg11 changed the base branch from main to brophdawg11/rmx-replace-navigation August 6, 2026 16:08
@brophdawg11
brophdawg11 force-pushed the brophdawg11/codex-spa-router branch from 2f2c826 to 9cc265e Compare August 7, 2026 18:57
Assisted-By: devx/755516d2-c45b-45bf-b52b-d964afb579bb
@brophdawg11
brophdawg11 force-pushed the brophdawg11/codex-spa-router branch from 9cc265e to 05f7584 Compare August 7, 2026 19:04
Assisted-By: devx/755516d2-c45b-45bf-b52b-d964afb579bb
Assisted-By: devx/755516d2-c45b-45bf-b52b-d964afb579bb
@brophdawg11
brophdawg11 force-pushed the brophdawg11/codex-spa-router branch from 9c3e428 to d56d265 Compare August 7, 2026 19:23
@brophdawg11
brophdawg11 marked this pull request as ready for review August 7, 2026 19:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant