-
-
Notifications
You must be signed in to change notification settings - Fork 0
HttpStatusProvider
-
Name:
HttpStatusProvider -
Purpose: Wrap an SSR tree with a render-scoped
HttpStatusSink, exposed via context for descendant<HttpStatusCode>instances to write into. -
When to use: Once per server-side
render()call, around the root tree, before rendering. The SSR entry readssink.codeafterawait render()to set the HTTP response status. -
Availability: All 5 framework adapters —
@real-router/react/ssr,@real-router/preact/ssr,@real-router/solid/ssr,@real-router/vue/ssr,@real-router/svelte/ssr. Semantics identical across adapters; Svelte usessetContext.
<script lang="ts">
import { HttpStatusProvider, createHttpStatusSink } from "@real-router/svelte/ssr";
const sink = createHttpStatusSink();
</script>
<HttpStatusProvider {sink}>
{#snippet children()}
<App />
{/snippet}
</HttpStatusProvider>Descendant <HttpStatusCode code={404} /> writes 404 into sink.code. The SSR entry reads sink.code after await render(...):
const html = await render(App, { props: { sink } });
res.status(sink.code ?? 200).send(html);| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
sink |
HttpStatusSink |
Yes | — | Mutable sink to capture the status. Construct one per request via createHttpStatusSink(). |
children |
Snippet |
Yes | — | Tree to render inside the provider. |
<script lang="ts">
import { setContext } from "svelte";
import { HTTP_STATUS_KEY } from "../context";
setContext(HTTP_STATUS_KEY, sink);
</script>
{@render children()}The provider calls setContext(HTTP_STATUS_KEY, sink) synchronously during init. Descendant <HttpStatusCode> instances read via getContext<HttpStatusSink | undefined>(HTTP_STATUS_KEY) and write sink.code = code once each.
The sink is render-scoped. Construct one per request via createHttpStatusSink():
export async function handler(req, res) {
const sink = createHttpStatusSink(); // fresh per request
const html = await render(App, { props: { sink } });
res.status(sink.code ?? 200).send(html);
}Do NOT share a sink across requests. Module-level singletons would leak status between concurrent requests.
<HttpStatusCode> writes sink.code = code during render. A frozen sink would throw TypeError: Cannot assign to read only property 'code' under ESM strict mode. Locked by property test tests/property/httpStatusSink.properties.ts Inv4.
getContext walks the context chain, finds the innermost matching key. Nesting providers is supported but uncommon — typical setup has one provider at the root:
<HttpStatusProvider sink={outerSink}>
{#snippet children()}
<!-- Outer sink available here. -->
<HttpStatusProvider sink={innerSink}>
{#snippet children()}
<!-- Inner sink wins; <HttpStatusCode> writes here. -->
<HttpStatusCode code={404} /> <!-- innerSink.code = 404 -->
{/snippet}
</HttpStatusProvider>
{/snippet}
</HttpStatusProvider>Tests pin nested-provider behavior — see tests/functional/HttpStatusCode.test.ts:82-91.
Typical SSR + CSR setup mounts the provider only on the server:
<!-- App.svelte -->
<script lang="ts">
let { router, sink } = $props();
</script>
{#if sink}
<HttpStatusProvider {sink}>
{#snippet children()}
<RootRoute {router} />
{/snippet}
</HttpStatusProvider>
{:else}
<RootRoute {router} />
{/if}Svelte 5's hydration walker tolerates the asymmetric {#if sink} branch — the server tree includes the provider, the client tree doesn't, and hydration succeeds without warnings. This is verified by examples/web/svelte/ssr-examples/ssr/ e2e tests.
Vue/Solid require symmetric provider mounting; Svelte's tolerance is a feature of its hydration walker.
// entry-server.ts
import { render } from "svelte/server";
import App from "./App.svelte";
import { createHttpStatusSink } from "@real-router/svelte/ssr";
import { createRouter } from "@real-router/core";
export async function handle({ event, resolve }) {
const router = createRouter(routes);
await router.start(event.url.pathname);
const sink = createHttpStatusSink();
const output = await render(App, {
props: { router, sink },
});
return new Response(output.body, {
status: sink.code ?? 200,
headers: { "content-type": "text/html" },
});
}<!-- App.svelte -->
<script lang="ts">
import {
HttpStatusProvider,
type HttpStatusSink,
} from "@real-router/svelte/ssr";
import { RouterProvider } from "@real-router/svelte";
import RootRoutes from "./RootRoutes.svelte";
import type { Router } from "@real-router/core";
let {
router,
sink,
}: { router: Router; sink?: HttpStatusSink } = $props();
</script>
{#if sink}
<HttpStatusProvider {sink}>
{#snippet children()}
<RouterProvider {router}>
<RootRoutes />
</RouterProvider>
{/snippet}
</HttpStatusProvider>
{:else}
<RouterProvider {router}>
<RootRoutes />
</RouterProvider>
{/if}<!-- routes/Product.svelte -->
<script lang="ts">
import { HttpStatusCode } from "@real-router/svelte/ssr";
import { useRoute } from "@real-router/svelte";
const { route } = useRoute();
const product = route.current.context.data?.product;
</script>
{#if !product}
<HttpStatusCode code={404} />
<h1>Product not found</h1>
{:else}
<ProductPage {product} />
{/if}// WRONG — sink is per-request.
const sink = createHttpStatusSink(); // module-level
export async function handler(req, res) {
// sink leaks between concurrent requests.
const html = await render(App, { props: { sink } });
res.status(sink.code ?? 200).send(html);
}// RIGHT — fresh sink per request.
export async function handler(req, res) {
const sink = createHttpStatusSink();
const html = await render(App, { props: { sink } });
res.status(sink.code ?? 200).send(html);
}- Per-request only. Don't reuse the sink. Module-level singletons leak status between concurrent requests. Pinned by property test Inv1 (fresh per call, cross-write isolation).
-
Don't freeze.
Object.freeze(sink)causes write failures under ESM strict mode. -
Provider runs once at component init. Replacing
sinkmid-render isn't a supported pattern — thesetContextcall is captured once. To switch sinks, remount the provider. -
Last write wins. Multiple
<HttpStatusCode />in the same render write to the same sink in template order; later writes overwrite earlier ones. -
getContextis read by descendants once per instance. Once a<HttpStatusCode>reads the sink at init, it doesn't re-read on reactivity. Replace the sink → no effect on already-mounted<HttpStatusCode>instances.
-
HttpStatusSinktype from@real-router/svelte/ssr(factory:createHttpStatusSink). - Svelte 5.x for
setContext+ runes. - No
<RouterProvider>requirement —<HttpStatusProvider>is independent of routing.
- HttpStatusCode — the descendant component that writes to the sink
- Svelte Integration Guide — overview of the SSR surface
- Server-Side Rendering — full SSR setup guide
-
createHttpStatusSink()— sink factory; construct one per request
- View-Agnostic Design
- Core Concepts
- Navigation Lifecycle
- Guards
- Plugin Architecture
- Hash Fragment Support
- Accessibility (A11y)
- Server-Side Rendering
- Data Loading
- Streaming SSR
- SSR Cancellation
- RSC Integration
- Testing
- Glossary
Tree-shakeable functions — import only what you need.
- add (addRoute)
- remove (removeRoute)
- replace (replaceRoutes)
- clear (clearRoutes)
- get (getRoute)
- has (hasRoute)
- update (updateRoute)
- get (getDependency)
- getAll (getDependencies)
- set (setDependency)
- setAll (setDependencies)
- has (hasDependency)
- remove (removeDependency)
- reset (resetDependencies)
For plugin authors, not for general use.
- makeState
- buildState
- buildNavigationState
- forwardState
- getForwardState
- setForwardState
- matchPath
- setRootPath
- getRootPath
- navigateToState
- addEventListener
- getRouteConfig
- getOptions
- addBuildPathInterceptor
- extendRouter
- getTree
- React Integration Guide
- Preact Integration Guide
- Solid Integration Guide
- Vue Integration Guide
- Svelte Integration Guide
- Ink (Terminal UI) Integration Guide
- Desktop (Electron, Tauri) Integration Guide
- useRouter
- useRoute
- useRouteNode
- useNavigator
- useRouteUtils
- useRouterTransition
- useRouteExit
- useRouteEnter
- useRouteStore (Solid only)
- useRouteNodeStore (Solid only)
SSR-feature subpath —
@real-router/{adapter}/ssr. Symmetric across React/Preact/Solid/Vue/Svelte.
- Lazy (Svelte only — dynamic component import)
- Await — read deferred SSR data by key
- Streamed — Suspense-style boundary
- ClientOnly — server fallback → client children swap
- ServerOnly — server children, removed after hydration
- HttpStatusCode — render-time HTTP status declaration
-
HttpStatusProvider — provides sink to descendant
<HttpStatusCode> - useDeferred — read deferred Promise by key