@real-router/ssr-data-plugin
v0.3.4
Published
SSR per-route data loading plugin for Real-Router
Readme
@real-router/ssr-data-plugin
Per-route data loading for SSR with Real-Router. Intercepts
start()to load data before server rendering.
// Without plugin:
const state = await router.start(url);
const data = await loadRouteData(state.name, state.params); // manual
// With plugin:
router.usePlugin(ssrDataPluginFactory(loaders));
const state = await router.start(url);
const data = state.context.data; // loaded automaticallyInstallation
npm install @real-router/ssr-data-pluginPeer dependencies: @real-router/core, @real-router/types
Quick Start
import { createRouter } from "@real-router/core";
import { cloneRouter } from "@real-router/core/api";
import { ssrDataPluginFactory } from "@real-router/ssr-data-plugin";
import type { DataLoaderFactoryMap } from "@real-router/ssr-data-plugin";
const loaders: DataLoaderFactoryMap = {
"users.profile": () => async (params) => fetchUser(params.id),
"users.list": () => async () => fetchUsers(),
};
// Base router — created once
const baseRouter = createRouter(routes, { defaultRoute: "home", allowNotFound: true });
// Per-request SSR
const router = cloneRouter(baseRouter, { isAuthenticated: true });
router.usePlugin(ssrDataPluginFactory(loaders));
const state = await router.start(url);
const data = state.context.data; // data loaded by matching loader
const html = renderToString(<App />);
router.dispose();Configuration
Loaders are keyed by route name (not path). Each value is a factory function (router, getDependency) => loaderFn that receives the router instance and a dependency getter. The factory runs once at plugin registration; the returned loader is cached. Each loader receives route params and returns a Promise:
import type { DataLoaderFactoryMap } from "@real-router/ssr-data-plugin";
const loaders: DataLoaderFactoryMap = {
home: () => async () => ({ featured: await fetchFeatured() }),
"users.profile": () => async (params) => ({ user: await fetchUser(params.id) }),
"users.list": () => async () => ({ users: await fetchUsers() }),
};Routes without a matching loader produce no data — state.context.data is undefined.
Accessing Data
After await router.start(url), data is available on the returned state's context:
const state = await router.start(url);
const data = state.context.data; // loaded data, or undefined if no loader matchedThe plugin claims the "data" namespace on state.context via the claim-based API. Module augmentation on @real-router/types provides type safety for state.context.data.
SSR-Only by Design
This plugin intercepts start() only — not navigate(). In SSR, the flow is:
cloneRouter → usePlugin → start(url) → data loaded → state.context.data → renderToStringClient-side navigation and data fetching is the application's responsibility (React Query, Suspense, useEffect, etc.).
Cleanup
const unsubscribe = router.usePlugin(ssrDataPluginFactory(loaders));
// Later — releases "data" namespace claim and stops data loading
unsubscribe();In SSR, router.dispose() handles cleanup automatically.
Documentation
- ARCHITECTURE.md — Design decisions and data flow
- SSR Example — Full working example with React + Vite
Related Packages
| Package | Description |
| ------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------- |
| @real-router/core | Core router (required peer dependency) |
| @real-router/rsc-server-plugin | Sibling plugin — same start() interceptor pattern but for ReactNode (RSC payload). Runs side-by-side on the same router with distinct namespaces (data vs rsc). |
| @real-router/browser-plugin | Browser History API integration |
| @real-router/react | React bindings |
