# Framework-Mode Integration (React Router v7)
This guide shows how to use `@react-router-modules/runtime` alongside React Router v7 framework mode (`@react-router/dev/vite`). The Vite plugin owns route discovery, type generation, HMR, and SSR/client splits; the registry owns everything else — shared dependencies, slots, navigation, zones, module lifecycle.
**For a new React Router v7 app, this is the recommended path.** The alternative (`registry.resolve()`) gives up a lot.
## Framework mode vs `resolve()`
`resolve()` calls `createBrowserRouter(routes)` directly. It's the shortest path to a working app — one call and you're rendering — but you give up everything `@react-router/dev/vite` provides:
| Feature | Framework mode (`resolveManifest`) | `resolve()` |
| ------------------------------------------------------------------------- | ---------------------------------- | -------------------- |
| HMR on route files | ✅ | ❌ — full reload |
| Generated `+types/route.ts` (typed params/loaders) | ✅ | ❌ |
| File-based route discovery (`flatRoutes()`) | ✅ | ❌ — imperative only |
| SSR / client-splits | ✅ | ❌ |
| `route() / index() / prefix()` ergonomics | ✅ | ❌ |
| Library owns router creation1 | ❌ | ✅ |
| Single-file wiring2 (app's full shape in one `resolve()` call) | ❌ | ✅ |
1 In framework mode, the library _intentionally_ defers router creation to `@react-router/dev/vite` so it can keep the framework's route discovery, type generation, and dev-server features. The ❌ here is the tradeoff that unlocks everything above — not a regression.
2 The ❌ is similarly the inverse of the ✅s above: when file-based discovery and generated types are owned by the framework, route shape lives in `routes.ts` instead of a `resolve()` call. Most apps find this a net win; plugin-host apps that need every module's full shape in one place are the counter-case.
Pick `resolve()` only when the tradeoff genuinely favors it:
- **Plugin-host apps** where modules arrive at runtime (external bundles, remote federation) and you can't pre-declare them in `routes.ts`.
- **CSR-only tools** with no need for typed params, SSR, or route-file HMR — a tiny internal dashboard where the one-call wiring is the point.
- **Legacy React Router setups** (pre-framework-mode) that haven't migrated yet. `resolve()` exists so you don't have to migrate to use this library.
For everything else — greenfield apps, anything shipping to real users, anything that benefits from typed routes — use `resolveManifest()`. The setup is a few more lines (one `registry.ts`, one `root.tsx` wrap, one `routes.ts`), and you keep the full React Router developer experience.
Read [Getting started with React Router](getting-started-react-router.md) first for the library-agnostic tour of modules and slots. This document focuses on the integration seam.
## What `resolveManifest()` does
`resolveManifest()` returns everything the registry can assemble **without creating a router**:
```ts
interface ResolvedManifest {
Providers: React.ComponentType<{ children: React.ReactNode }>;
routes: RouteObject[];
navigation: NavigationManifest;
slots: TSlots;
modules: readonly ModuleEntry[];
recalculateSlots: () => void;
}
```
- `Providers` wraps the full modular-react context stack — shared deps, navigation, slots, modules, recalculate signal, and any `providers?` option you passed. Place it around `` in your root layout.
- `routes` holds any routes modules contribute via `createRoutes()`. Empty array if no module declares routes — the common case when route shape lives in `routes.ts`.
- The rest matches `resolve()`.
No `DataRouter` is created. The framework Vite plugin bootstraps the router as usual.
## The idempotent registry
`resolveManifest()` is idempotent — call it as many times as you want. The first call does the work (validation, `onRegister` hooks, route building, provider wiring) and caches the result. Later calls return the same manifest.
**Options are honored only on the first call.** Passing options on a subsequent call throws, so misconfiguration is loud instead of silently ignored. The recommended pattern is to resolve once in a shared module and import it from both `routes.ts` and `root.tsx`:
```ts
// app/registry.ts
import { createRegistry } from "@react-router-modules/runtime";
import portalModule from "./modules/portal";
import type { AppDependencies, AppSlots } from "./types";
import { I18nProvider } from "./providers/i18n";
import { authStore, httpClient } from "./services";
const registry = createRegistry({
stores: { auth: authStore },
services: { httpClient },
slots: { commands: [] },
});
registry.register(portalModule);
export const manifest = registry.resolveManifest({
providers: [I18nProvider],
});
```
```ts
// app/root.tsx
import { Outlet } from "react-router"
import { manifest } from "./registry"
export default function Root() {
return (
)
}
```
```ts
// app/routes.ts
import type { RouteConfig } from "@react-router/dev/routes";
import { flatRoutes } from "@react-router/fs-routes";
import { route, index } from "@react-router/dev/routes";
// Routes live in framework-mode primitives — the host owns route shape.
export default [
...(await flatRoutes({ ignoredRouteFiles: ["portal/**"] })),
route("portal", "routes/portal/layout.tsx", [
index("routes/portal/index.tsx"),
route(":workspaceId/requests", "routes/portal.workspace.requests.tsx"),
]),
] satisfies RouteConfig;
```
Note that nothing in `routes.ts` references the registry. That's intentional — route **shape** is declared by the host using framework primitives; the module contributes navigation, slots, zones, and lifecycle, not route file paths.
## Mixing library- and host-owned routes
A module can still return `RouteObject[]` from `createRoutes()` if it wants to. Those routes surface on `manifest.routes` and the host can mount them anywhere:
```ts
// app/routes.ts
import { manifest } from "./registry";
export default [
...(await flatRoutes()),
// Mount module-contributed routes under a catch-all the host owns:
route("plugins/*", "routes/plugins-root.tsx"),
] satisfies RouteConfig;
```
```tsx
// app/routes/plugins-root.tsx
import { useRoutes } from "react-router";
import { manifest } from "../registry";
export default function PluginsRoot() {
return useRoutes(manifest.routes);
}
```
This is a useful pattern for plugin registries where external modules deliver routes at runtime. For modules shipped as part of the app, declare their shape in `routes.ts` instead — you keep generated types and HMR.
## Auth, 404, shell routes in framework mode
All of these move out of `resolveManifest()` options and into `routes.ts` / loaders:
| `resolve()` option | Framework-mode equivalent |
| --------------------- | ---------------------------------------------------------------------------------- |
| `rootComponent` | `app/root.tsx` |
| `indexComponent` | `index("routes/home.tsx")` in `routes.ts` |
| `notFoundComponent` | `route("*", "routes/not-found.tsx")` in `routes.ts` |
| `authenticatedRoute` | `layout("routes/_auth.tsx", [...])` in `routes.ts` with a `loader` for the guard |
| `shellRoutes` | Regular entries in `routes.ts`, outside the auth layout |
| `loader` (root-level) | `loader` export in `app/root.tsx` |
| `providers` | Still on `resolveManifest({ providers })` — applied to the context tree |
| `slotFilter` | Still on `resolveManifest({ slotFilter })` — applied to the dynamic-slots pipeline |
The two options that remain on `resolveManifest()` — `providers` and `slotFilter` — are about the context tree, not about routing. They stay because the `Providers` component owns them.
### `_auth.tsx` — minimal layout-route auth guard
Concrete sketch of the framework-mode equivalent to `authenticatedRoute` on `resolve()`:
```ts
// app/routes.ts
import type { RouteConfig } from "@react-router/dev/routes";
import { layout, route, index } from "@react-router/dev/routes";
import { flatRoutes } from "@react-router/fs-routes";
export default [
route("login", "routes/login.tsx"),
route("signup", "routes/signup.tsx"),
layout("routes/_auth.tsx", [
index("routes/home.tsx"),
// Module routes — file-based discovery for everything under the auth
// boundary. Module files contribute `handle` / `loader` / components
// as normal; the guard in `_auth.tsx` gates them all.
...(await flatRoutes({ rootDirectory: "routes/protected" })),
]),
] satisfies RouteConfig;
```
```tsx
// app/routes/_auth.tsx
import { Outlet, redirect } from "react-router";
import type { Route } from "./+types/_auth";
export async function loader({ request }: Route.LoaderArgs) {
const res = await fetch(new URL("/api/auth/session", request.url), {
headers: { cookie: request.headers.get("cookie") ?? "" },
});
if (!res.ok) throw redirect("/login");
const session = await res.json();
return { session };
}
export default function AuthLayout() {
// The loader already gated entry; everything nested under this route is
// authenticated. Replace with your real shell layout if you
// had a Component on `authenticatedRoute`.
return ;
}
```
Under `resolve()`, the library built this tree for you. In framework mode, you declare it with `layout()` + a regular `loader` export — fewer library concepts, fully typed by the framework's generated `+types/_auth`.
## Testing
`resolveManifest()` is fully testable without a router. The `Providers` component mounts the same context stack `resolve()` uses, so hooks like `useNavigation`, `useSlots`, `useModules`, and `useStore` work in tests that only render ``:
```tsx
import { render } from "@testing-library/react";
import { createRegistry } from "@react-router-modules/runtime";
import { useNavigation } from "@modular-react/react";
const registry = createRegistry({ stores: { auth: authStore }, services: { httpClient } });
registry.register(billingModule);
const { Providers } = registry.resolveManifest();
function Probe() {
const nav = useNavigation();
return (
{nav.items.map((i) => (
- {i.label}
))}
);
}
const { getByText } = render(
,
);
expect(getByText("Billing")).toBeInTheDocument();
```
For tests that exercise real routing, use the existing `@react-router-modules/testing` utilities.
## Rules of thumb
- **Pick a mode early.** The registry commits on first call — mixing `resolve()` and `resolveManifest()` throws. Decide whether the library or the host owns the router before you start registering modules.
- **Resolve once.** Put `resolveManifest()` in a shared module (`app/registry.ts` or similar) and import the manifest from every callsite. The idempotency safety net exists so a slip-up is loud, not so you have a license to scatter calls.
- **Route shape in `routes.ts`, everything else in modules.** Modules still own navigation, slots, zones, lifecycle, and shared-deps requirements. They just stop declaring their own `createRoutes()` — route files live where the framework Vite plugin can find them.
- **Typed DI and startup validation.** Both work identically: `createSharedHooks()` and `requires: [...]` do what they always did. `Providers` delivers the dependency container; no router involvement.
## See also
- [Framework-mode integration (TanStack Router & Start)](framework-mode-tanstack-router.md) — the TanStack-side equivalent, including SSR considerations.
- [Getting started with React Router](getting-started-react-router.md) — for the library-owns-router path.
- [Shell Patterns for React Router](shell-patterns-react-router.md) — module route shape, zones, auth guards.
- [`useRouteData`](shell-patterns-react-router.md#route-data-non-component-handles) — non-component route metadata (headerVariant, page titles).
- [Navigation: typed labels, dynamic hrefs, meta](navigation.md) — the full `NavigationItem` generic surface.