/ Docs

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.