Class: ReactRoute
Defined in: ReactRoute.tsx:198
Base class for HTTP routes that serve React pages from the app/ directory.
Convention-based page routing:
app/_layout.tsx— global HTML wrapper (loaded once, required)app/_404.tsx— 404 error page (optional)app/_500.tsx— 500 error page (optional)app/_shell.tsx— persistent client shell around the page, kept mounted between pages by the router (optional; only withrouter = true)app/pets.tsx— serves GET /petsapp/pets/index.tsx— also serves GET /pets (index convention)app/pets/[id].tsxorapp/pets/[id]/index.tsx— serves GET /pets/:id; the captured value is available asreq.params.id(infetchProps) andprops.params.id(on the component)app/_styles/— CSS assets (imported by layout or page components)- Any other non-
_-prefixed.tsxfile, at any depth, is a page — colocate a shared/helper component under an_-prefixed file or directory name to keep it from becoming a route
Each page file exports:
default— React component (required)fetchProps— async function (req) → props object (optional)title— the document's<title>for the page: a string, or a function of the page's props returning one (optional; goes ahead of the layout's own<title>)getStaticPaths— async function () → array of{ [param]: string }objects, one per concrete instance of a dynamic route this page should statically export (optional; a@ReactServicematching the same dynamic route may also implementgetStaticPaths()for DI-backed enumeration — seeexportStaticSite()instatic.ts)
Subclass to inject DI services into fetchProps:
@Route("/*")
export class AppRouter extends ReactRoute {
@Inject(PetService) private pets: PetService;
protected async fetchProps(req) { return { pets: await this.pets.findAll() }; }
}
Constructors
Constructor
new ReactRoute():
ReactRoute
Returns
ReactRoute
Properties
appDir
protectedreadonlyappDir:string="app"
Defined in: ReactRoute.tsx:208
Filesystem path to the app directory, relative to cwd. Default is app.
cache?
protectedoptionalcache?:RedisStore
Defined in: ReactRoute.tsx:204
cacheTTL
protectedreadonlycacheTTL:number=60
Defined in: ReactRoute.tsx:211
Cache TTL in seconds. Caching is only active in production (NODE_ENV=production).
hydrate
protectedreadonlyhydrate:boolean=false
Defined in: ReactRoute.tsx:214
Opt-in client-side hydration. When true, wraps the page in a hydration root and injects the client bundle.
hydratePropsId
protectedreadonlyhydratePropsId:string="react-props"
Defined in: ReactRoute.tsx:245
DOM element id for the serialized props <script> tag.
hydrateRootId
protectedreadonlyhydrateRootId:string="react-root"
Defined in: ReactRoute.tsx:242
DOM element id for the React hydration root.
logger
protectedlogger:any
Defined in: ReactRoute.tsx:298
maxDevReloadConnections
protectedreadonlymaxDevReloadConnections:number=100
Defined in: ReactRoute.tsx:248
Hard cap on concurrent dev-reload SSE connections per route instance.
router
protectedreadonlyrouter:boolean=false
Defined in: ReactRoute.tsx:232
Opt-in client-side navigation. When true, the pages of this app hydrate under a router that, on a click on a link
to another of the app's pages, fetches that page's props as JSON (the same props the server would have rendered it
with) and swaps the page in, instead of loading a whole new document. Implies hydrate, and needs the client
bundle built with createViteConfig({ router: true }).
The router only ever enhances rendering the page on the server, which it never replaces: every page is still
served as complete HTML at its own URL (so it works with JavaScript off, for crawlers, and as the first load),
and whenever the client isn't sure it can navigate faithfully — an unknown route, a redirect, an error page, any
failure — the browser simply loads the URL the ordinary way. See routerCore.ts.
A router app can also have a persistent shell, _shell.tsx next to _layout.tsx: a client layout, rendered inside
the hydration root around the page and kept mounted as the router swaps the pages in and out inside it. It gets the
page's props and the page as children. It is used only under the router (there is nothing to keep mounted without
one), never around the _404/_500 pages (which aren't hydrated), and it isn't a page of its own.
userFields
protectedreadonlyuserFields:string[] |null=null
Defined in: ReactRoute.tsx:239
Allow-list of req.user field names exposed as props.user. Default null means no
user object is exposed to pages or the hydration payload — only the scalar userUid is
passed. Opt in per-subclass, e.g. protected readonly userFields = ["uid", "email"];.
Methods
cacheKeyExtras()
protectedcacheKeyExtras(_req):Record<string,any>
Defined in: ReactRoute.tsx:438
Override to fold additional request dimensions (e.g. Accept-Language, a feature-flag
cookie) into the cache key, for pages whose rendered output varies on something beyond
path/params/query/user identity. Returns {} by default (no extra dimensions).
Parameters
_req
HttpRequest
Returns
Record<string, any>
canonicalizeQuery()
protectedcanonicalizeQuery(query):Record<string,string|string[]>
Defined in: ReactRoute.tsx:419
Produces a stable, key-order-independent representation of req.query for cache-key hashing. Sorting only the top-level keys (not array element order) avoids new collisions between semantically different multi-value query strings while eliminating key-order nondeterminism.
Parameters
query
Record<string, string | string[]> | undefined
Returns
Record<string, string | string[]>
escapeForInlineScript()
protectedescapeForInlineScript(json):string
Defined in: ReactRoute.tsx:1524
Escapes a JSON string for safe embedding inside an inline <script> element. Replacing every
< (not just the literal substring "</script>") blocks the full HTML end-tag-open grammar
(</script followed by whitespace, /, or >), which a substring match on "</script>" alone
does not. JSON.stringify never emits a bare < outside of string content, so this is safe
and JSON.parse treats < identically to <, so it round-trips losslessly.
Parameters
json
string
Returns
string
fetchProps()
protectedfetchProps(_req):Promise<any>
Defined in: ReactRoute.tsx:1178
Returns additional props merged into the page component props.
Override in subclasses to provide DI-injected server-side data.
Page-file fetchProps runs first; this override runs after and takes precedence.
Parameters
_req
HttpRequest
Returns
Promise<any>
get()
get(
req,res):Promise<any>
Defined in: ReactRoute.tsx:966
Parameters
req
HttpRequest
res
HttpResponse
Returns
Promise<any>
getIndex()
getIndex(
req,res):Promise<any>
Defined in: ReactRoute.tsx:1069
Parameters
req
HttpRequest
res
HttpResponse
Returns
Promise<any>
hashRequest()
protectedhashRequest(req):string
Defined in: ReactRoute.tsx:447
Computes a stable per-request cache key (MD5 of the app, path + params + query + user identity). The app
(appDir and mount prefix) is part of it so two apps sharing one cache can't answer each other's URLs, and under
the router so is the query string as written, since the page renders the location it was asked for.
Parameters
req
HttpRequest
Returns
string
init()
protectedinit():Promise<void>
Defined in: ReactRoute.tsx:329
Returns
Promise<void>
isDevMode()
protectedisDevMode():boolean
Defined in: ReactRoute.tsx:523
Whether dev-only behaviors (live-reload SSE endpoint, reload script injection, manifest
file-watching) are enabled. Explicit allow-list rather than !== "production" so an unset
or misconfigured NODE_ENV in a real deployment fails closed (dev features OFF) instead of
failing open. Override in a subclass to opt a non-standard environment name into dev mode.
Returns
boolean
pickUserFields()
protectedpickUserFields(user):Record<string,any> |undefined
Defined in: ReactRoute.tsx:1164
Picks the userFields allow-list out of user. Returns undefined (meaning: don't expose
a user prop at all) when there's no user or no allow-list configured.
Parameters
user
any
Returns
Record<string, any> | undefined
resolveAppFile()
protectedresolveAppFile(appDir,segment,internal?):Promise<ResolvedAppFile|null>
Defined in: ReactRoute.tsx:545
Resolve a file in the app directory for a URL path segment (e.g. /pets or /pets/123),
walking one path component at a time. At each directory level a literal name match wins;
only on a literal miss does a [name]-bracketed sibling (a dynamic-segment directory or, at
the final segment, a leaf file) get tried, capturing the URL value into params. There is no
backtracking: once a literal directory is entered at a level, a subsequent resolution failure
fails outright rather than retrying a sibling bracket at that level.
At the final segment, tries .tsx → /index.tsx → .jsx → /index.jsx → .js →
/index.js (dev) or .js → /index.js (production) in order, same as before — this ordering
is now also applied to a bracket-name candidate, treating it exactly like a literal segment.
A file or directory whose name starts with _ is never a route, so no URL resolves to one: /_layout and
/_components/Button are as unmatched as any other missing page (a [dynamic] sibling can still capture the
value). internal is for the framework's own lookups of _layout, _404 and _500, which are exactly those
files: it matches the literal name only, never a [dynamic] sibling.
Parameters
appDir
string
segment
string
internal?
boolean = false
Returns
Promise<ResolvedAppFile | null>