Skip to content

Middleware

Middleware in λ Router are React components that wrap route content. They receive children as a prop and can render them conditionally, add layout elements, or provide context. Middleware is the recommended pattern for auth guards, layout shells, and any cross-cutting concerns.

A middleware is any React component that accepts children and renders them (or not):

import type { MiddlewareProps } from "@studiolambda/router/react";
function AuthGuard({ children }: MiddlewareProps) {
const user = useCurrentUser();
if (!user) {
return <LoginPage />;
}
return children;
}

The MiddlewareProps type is simply { children: ReactNode }.

Use the .middleware() method on the route builder. It accepts an array of middleware components:

import { createRouter } from "@studiolambda/router/react";
const router = createRouter(function (route) {
route("/").render(Home);
route("/dashboard")
.middleware([AuthGuard])
.render(Dashboard);
route("/admin")
.middleware([AuthGuard, AdminOnly])
.render(AdminPanel);
});

When multiple middleware are specified, they are applied from outermost to innermost — the first element in the array wraps the outermost:

// For .middleware([AuthGuard, AdminOnly]):
// Renders as:
<AuthGuard>
<AdminOnly>
<AdminPanel />
</AdminOnly>
</AuthGuard>

Groups share middleware across multiple routes. Parent group middleware wraps outermost, followed by child route middleware:

const router = createRouter(function (route) {
route("/").render(Home);
const authed = route().middleware([AuthGuard]).group();
authed("/profile").render(Profile);
authed("/settings").render(Settings);
// Nested groups inherit parent middleware.
const admin = authed("/admin").middleware([AdminOnly]).group();
admin("/users").render(AdminUsers); // AuthGuard → AdminOnly → AdminUsers
admin("/config").render(AdminConfig); // AuthGuard → AdminOnly → AdminConfig
});

Navigating to /admin/users renders:

<AuthGuard>
<AdminOnly>
<AdminUsers />
</AdminOnly>
</AuthGuard>

Middleware is a natural way to implement shared layouts:

function DashboardLayout({ children }: MiddlewareProps) {
return (
<div className="dashboard">
<DashboardSidebar />
<main>{children}</main>
</div>
);
}
const router = createRouter(function (route) {
const dashboard = route("/dashboard")
.middleware([AuthGuard, DashboardLayout])
.group();
dashboard("/").render(DashboardHome);
dashboard("/analytics").render(Analytics);
dashboard("/settings").render(Settings);
});

Every route under /dashboard renders inside the shared sidebar layout, and only authenticated users can access it.

Middleware can conditionally render children based on any logic — permissions, feature flags, data availability:

function FeatureFlag({ children }: MiddlewareProps) {
const flags = useFeatureFlags();
if (!flags.newDashboard) {
return <LegacyDashboard />;
}
return children;
}
function RoleGuard({ children }: MiddlewareProps) {
const user = useCurrentUser();
if (user.role !== "admin") {
return <Forbidden />;
}
return children;
}

Middleware can provide context values to descendant components:

const ThemeContext = createContext("light");
function ThemeProvider({ children }: MiddlewareProps) {
const theme = useUserThemePreference();
return <ThemeContext value={theme}>{children}</ThemeContext>;
}
const router = createRouter(function (route) {
const app = route().middleware([ThemeProvider]).group();
app("/").render(Home);
app("/settings").render(Settings);
});

Middleware components can include their own Suspense boundaries for fine-grained loading states:

function DataLayout({ children }: MiddlewareProps) {
return (
<div className="layout">
<Header />
<Suspense fallback={<ContentSkeleton />}>{children}</Suspense>
<Footer />
</div>
);
}

This way, the header and footer remain visible while the route content is loading.