dev: Migrate to React Router Data mode#17
Open
isTravis wants to merge 3 commits into
Open
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Replaces the hand-rolled SSR system (manual route matching,
loaders.server.ts,useSSRDatacontext,window.__SSR_DATA__hydration) with React Router v7's Data Mode APIs (createStaticHandler,createBrowserRouter,useRouteLoaderData).Data Mode is the middle tier of React Router's three modes — it gives us loaders, actions, fetchers, and revalidation without taking over the server or build system. Hono, Vite, and the full
server.tsare structurally unchanged.What changed
Deleted:
loaders.server.ts(410 lines) — every loader did the same thing:getSessionUser(request)+getMirrorConfig()+ URL params the component already had viauseParams(). A single root loader replaces all 39.lib/ssr-data.tsx— custom React Context for SSR data, replaced by React Router's built-in data flow.New:
/api/contextendpoint — returns{ currentUser, mirrorConfig, kfAccountUrl, kfAuthUrl }. Used by the root loader during client-side navigations.lib/app-context.ts— typeduseAppContext()hook wrappinguseRouteLoaderData('root').lib/route-meta.ts— sharedextractRouteMeta()used by both entry points for title/description extraction.components/Root.tsx— invisible root route component (<Outlet />+ error boundary).Rewritten:
entry-server.tsx— usescreateStaticHandler+query(request, { requestContext })+StaticRouterProvider. PassesloadAppContextviarequestContextso the root loader callsgetSessionUserdirectly during SSR — no loopback HTTP request. Auth redirects checked viahandle.requireAuthafterquery().entry-client.tsx— usescreateBrowserRouter+RouterProviderwith hydration data. Pre-resolves matched lazy routes beforehydrateRootto prevent mismatch. Title updates viarouter.subscribe()using sharedextractRouteMeta().App.tsx— pure config: exports aroutesarray with an isomorphic root loader that branches oncontext(SSR: direct server call; client: fetch/api/context).route-gen.ts— newbuildDataRoutes()that outputsRouteObject[]withlazyfor code splitting.buildRoutes()kept for test backward compat.All 39 route files:
handleexports for{ title, requireAuth }— replaces the title/auth metadata that lived inloaders.server.ts.useSSRData(key)withuseAppContext().if (!me) return nullto<Navigate to="/login" replace />.What this gets us
Client-side navigations now fetch data. The old
useSSRData()was a dead-end — it only had data from the initial server render. Navigate to/dashboardfrom/exploreandcurrentUserwas stale. The root loader now runs on every navigation.Per-route data loading is now possible. If a route needs to prefetch data (e.g., collection metadata for
/:owner/:collection), it can export aloaderand useuseLoaderData(). The infrastructure is in place without any additional plumbing. Route loaders can userequestContextduring SSR for direct server access, falling back to API fetches on the client.Actions and mutations have a path.
useFetcher(),<Form>, and automatic revalidation after mutations are available. When we add write operations beyond rawfetch()calls, we don't need to build the lifecycle ourselves.Standard APIs.
createBrowserRouter,useRouteLoaderData,createStaticHandlerare well-documented, widely used, and what tools/LLMs expect to see in a React Router v7 app.Convention that scales across projects. This is the SSR pattern for all KF apps. The hand-rolled approach was ~300 lines of custom plumbing per project (entry-server, loaders, ssr-data, route matching). Data Mode replaces that with a standardized setup backed by React Router's own APIs.
How SSR data loading works
The root loader is isomorphic — the same function runs during SSR and client navigation — but it branches on whether the server provided a
requestContext:Per-route loaders follow the same pattern: use
requestContextfor direct server access during SSR, fall back to API fetches on the client.Tradeoffs and known rough edges
Auth is split across two paths. Server-side:
entry-server.tsxcheckshandle.requireAuthafterquery()and returns a 302. Client-side: route components render<Navigate to="/login">. Same concern, two implementations. The old system had one code path (loader redirects). This is a Data Mode limitation — loaders can't do server-only auth checks and also run on the client.Lazy route hydration needs a workaround.
entry-client.tsxpre-resolves matchedlazyroutes before callinghydrateRoot. Without this,RouterProviderrendersnullwhile lazy modules load, causing a hydration mismatch (content doubling). The fix is correct but non-obvious — it needs a comment and awareness from anyone touching the client entry.currentUser: anyin AppContext. The old code usedanyeverywhere for the user object. The typedAppContextinterface initially surfaced missing fields (bio,website,location) thatSessionUserdoesn't include. Rather than audit all consumer sites,currentUserstaysanyfor now. Worth tightening later.