Building a Router
A Router aggregates routes and adds cross-cutting concerns like layouts, guards, fallback pages, and error handling. You build one by starting with Router.empty and piping combinators.
Adding Routes
Use Router.concat to add routes one at a time:
import { Router } from "@effex/router";
const router = Router.empty.pipe(
Router.concat(HomeRoute),
Router.concat(AboutRoute),
Router.concat(UserRoute),
);
Router.concat also accepts another Router, so you can compose sub-routers:
const adminRouter = Router.empty.pipe(
Router.concat(AdminDashboardRoute),
Router.concat(AdminUsersRoute),
);
const router = Router.empty.pipe(
Router.concat(HomeRoute),
Router.concat(adminRouter),
);
Fallback (404)
Set a fallback component for when no route matches:
const router = Router.empty.pipe(
Router.concat(HomeRoute),
Router.concat(AboutRoute),
Router.fallback(() => NotFoundPage()),
);
The fallback renders whenever the current pathname doesn’t match any route.
Prefixing
Add a path prefix to all routes in a router. Useful for grouping related routes under a common path:
const adminRouter = Router.empty.pipe(
Router.concat(DashboardRoute), // "/dashboard"
Router.concat(UsersRoute), // "/users"
Router.prefixAll("/admin"),
);
// Routes: /admin/dashboard, /admin/users
Layouts
Wrap matched routes in a layout component. Layouts are applied inside-out — the first layout is innermost:
const dashboardRouter = Router.empty.pipe(
Router.concat(DashboardHomeRoute),
Router.concat(SettingsRoute),
Router.layout(SidebarLayout),
Router.layout(AppShell),
);
// Renders: AppShell(SidebarLayout(matchedRoute))
A layout function receives the matched route’s Element and must be transparent to its error and requirement types:
const SidebarLayout = <A extends HTMLElement | SVGElement, E, R>(
children: Element.Element<A, E, R>,
) =>
$.div(
{ class: "flex" },
collect(
Sidebar(),
$.main({ class: "flex-1" }, children),
),
);
The generic signature ensures that errors and context requirements from the matched route flow through the layout unchanged.
Router-Level Guards
Protect an entire group of routes with a guard:
const protectedRouter = Router.empty.pipe(
Router.concat(DashboardRoute),
Router.concat(ProfileRoute),
Router.concat(SettingsRoute),
);
const router = Router.empty.pipe(
Router.concat(HomeRoute),
Router.concat(LoginRoute),
Router.guard(isAuthenticated, protectedRouter, {
redirect: "/login",
}),
);
When isAuthenticated is false, any attempt to navigate to a protected route redirects to /login. The guard accepts either a Readable<boolean> or an Effect<boolean>.
You can also render a fallback instead of redirecting:
Router.guard(isAuthenticated, protectedRouter, {
fallback: () => LoginPage(),
});
Router-Level Error Handling
Catch errors from all routes in the router:
const router = Router.empty.pipe(
Router.concat(HomeRoute),
Router.concat(UserRoute),
Router.catchTag("NotFound", () => NotFoundPage()),
Router.catchTag("Unauthorized", () => LoginPage()),
);
This is equivalent to adding Route.catchTag to every individual route, but applied in one place.
Other Error Handlers
// Catch by predicate
Router.catchIf(
(e) => e._tag === "NetworkError",
() => OfflinePage(),
);
// Catch everything
Router.catchAll((error) => ErrorPage({ error }));
Full Example
import { Router } from "@effex/router";
// Public routes
const publicRouter = Router.empty.pipe(
Router.concat(HomeRoute),
Router.concat(LoginRoute),
Router.concat(SignupRoute),
);
// Admin section with its own layout and prefix
const adminRouter = Router.empty.pipe(
Router.concat(AdminDashboardRoute),
Router.concat(AdminUsersRoute),
Router.prefixAll("/admin"),
Router.layout(AdminLayout),
);
// Protected routes
const appRouter = Router.empty.pipe(
Router.concat(DashboardRoute),
Router.concat(ProfileRoute),
Router.concat(SettingsRoute),
Router.layout(AppLayout),
);
// Compose everything
export const router = Router.empty.pipe(
Router.concat(publicRouter),
Router.guard(isAuthenticated, appRouter, { redirect: "/login" }),
Router.guard(isAdmin, adminRouter, { redirect: "/" }),
Router.fallback(() => NotFoundPage()),
Router.catchAll((error) => ErrorPage({ error })),
);