React Router Integration
Overview
The @bounda-dev/react-router package integrates Bounda with React Router v7, giving your loaders and actions direct access to typed commands and queries through React Router’s context API.
Installation
npm i @bounda-dev/react-router
Setup
1. Configure the Vite Plugin
Add the Bounda Vite plugin alongside the React Router plugin in your vite.config.ts:
import { reactRouter } from "@react-router/dev/vite";
import { bounda } from "@bounda-dev/codegen/vite";
import { defineConfig } from "vite";
export default defineConfig({
plugins: [reactRouter(), bounda()],
});
The bounda() plugin runs code generation during development and build, keeping your generated types in sync with your domain definitions.
2. Update TypeScript Configuration
Add .bounda to your tsconfig.json so TypeScript can resolve the generated types:
{
"include": [".bounda/**/*", "app/**/*"],
"compilerOptions": {
"rootDirs": [".", ".bounda"]
}
}
3. Enable React Router Middleware
In your react-router.config.ts, enable the v8_middleware future flag:
export default {
future: { v8_middleware: true },
};
4. Register the Middleware
In your root route (app/root.tsx), register the Bounda middleware:
import { boundaMiddleware } from "@bounda-dev/react-router";
export const middleware: Route.MiddlewareFunction[] = [boundaMiddleware];
The middleware boots Bounda once on the first request and makes the application instance available through React Router’s context for all subsequent requests.
Usage
Dispatching Commands in Actions
Use context.get(boundaContext) to access the Bounda application instance. Commands are available on the commands property:
import { boundaContext } from "@bounda-dev/react-router";
import type { Route } from "./+types/register";
export async function action({ request, context }: Route.ActionArgs) {
const { commands } = context.get(boundaContext);
const formData = await request.formData();
await commands.registerUser({
aggregateId: crypto.randomUUID(),
email: String(formData.get("email")),
name: String(formData.get("name")),
});
return { success: true };
}
Running Queries in Loaders
Queries are available on the queries property:
import { boundaContext } from "@bounda-dev/react-router";
import type { Route } from "./+types/users";
export async function loader({ context }: Route.LoaderArgs) {
const { queries } = context.get(boundaContext);
return await queries.getUsers({ page: 1, pageSize: 10 });
}
Combining Commands and Queries
A single route can use both commands in its action and queries in its loader:
import { boundaContext } from "@bounda-dev/react-router";
import type { Route } from "./+types/order";
export async function loader({ params, context }: Route.LoaderArgs) {
const { queries } = context.get(boundaContext);
return await queries.getOrderSummary({ orderId: params.orderId });
}
export async function action({ request, params, context }: Route.ActionArgs) {
const { commands } = context.get(boundaContext);
await commands.confirmOrder({ aggregateId: params.orderId });
return { confirmed: true };
}
How It Works
The boundaMiddleware calls boot() from @bounda-dev/core on the first server-side request. The resulting application instance is cached for the lifetime of the process and injected into React Router’s context via boundaContext. Subsequent requests reuse the same instance without re-booting.
Note: The middleware only runs on the server. Attempting to use it in a client-side context throws an error.
Related
- Configuration Reference — Setting up
bounda.config.ts - Commands — Defining and dispatching commands
- Views — Building read models and queries
- CLI Reference — Code generation with
bounda generate