Skip to main content

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 with router = true)
  • app/pets.tsx — serves GET /pets
  • app/pets/index.tsx — also serves GET /pets (index convention)
  • app/pets/[id].tsx or app/pets/[id]/index.tsx — serves GET /pets/:id; the captured value is available as req.params.id (in fetchProps) and props.params.id (on the component)
  • app/_styles/ — CSS assets (imported by layout or page components)
  • Any other non-_-prefixed .tsx file, 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 @ReactService matching the same dynamic route may also implement getStaticPaths() for DI-backed enumeration — see exportStaticSite() in static.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​

protected readonly appDir: string = "app"

Defined in: ReactRoute.tsx:208

Filesystem path to the app directory, relative to cwd. Default is app.


cache?​

protected optional cache?: RedisStore

Defined in: ReactRoute.tsx:204


cacheTTL​

protected readonly cacheTTL: number = 60

Defined in: ReactRoute.tsx:211

Cache TTL in seconds. Caching is only active in production (NODE_ENV=production).


hydrate​

protected readonly hydrate: 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​

protected readonly hydratePropsId: string = "react-props"

Defined in: ReactRoute.tsx:245

DOM element id for the serialized props <script> tag.


hydrateRootId​

protected readonly hydrateRootId: string = "react-root"

Defined in: ReactRoute.tsx:242

DOM element id for the React hydration root.


logger​

protected logger: any

Defined in: ReactRoute.tsx:298


maxDevReloadConnections​

protected readonly maxDevReloadConnections: number = 100

Defined in: ReactRoute.tsx:248

Hard cap on concurrent dev-reload SSE connections per route instance.


router​

protected readonly router: 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​

protected readonly userFields: 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()​

protected cacheKeyExtras(_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()​

protected canonicalizeQuery(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()​

protected escapeForInlineScript(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()​

protected fetchProps(_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()​

protected hashRequest(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()​

protected init(): Promise<void>

Defined in: ReactRoute.tsx:329

Returns​

Promise<void>


isDevMode()​

protected isDevMode(): 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()​

protected pickUserFields(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()​

protected resolveAppFile(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>