The Werkbank Router is a type-safe, schema-first routing library for React that treats URL paths and query parameters as validated data structures.
The Werkbank Router inverts the traditional routing model. Instead of defining routes as strings and parsing them inside components, you define Schemas for your routes first.
valibot.Start by defining the structure of your application's URLs. A route definition consists of a Path Pattern and optional Search Parameters.
import { string, number, object } from "valibot";
// Define the static configuration
const routeConfig = {
// Simple static route: /dashboard
dashboard: ["/dashboard"],
// Dynamic route with parameter: /users/:userId
userProfile: ["/users/", string()],
// Route with search parameters: /search?q=query&page=1
search: {
path: ["/search"],
searchParams: object({
q: string(),
page: number(),
}),
},
// Nested routes: /settings/profile
settings: {
path: ["/settings"],
children: {
profile: ["/profile"],
},
},
} as const; // 'as const' is CRITICAL for type inference
For each route defined in step 1, you must provide an implementation. This includes the Component, and optionally a Preload function, Loading component, and Error boundary.
const Dashboard = () => <h1>Dashboard</h1>;
The preload function receives the parsed parameters, search parameters, and your global context.
Note: The
preloadfunction is designed to initiate data fetching (e.g., priming a cache or store). It does not pass data directly to the component. Your component should subscribe to the store or use a hook to access the data.
import type { PreloadFunction } from "@werkbank/router";
const UserProfile = ({ params }) => (
<div>User: {params[0]}</div>
);
// Preload data before rendering
const preloadUser: PreloadFunction<
[string], // Path params type (inferred automatically in real usage)
never, // Search params type
[], // Parent params
void // Context (optional)
> = async ({ params }) => {
// params[0] is typed as string because of the schema
console.log("Preloading user:", params[0]);
};
You can lazy load components to split your bundle.
const SearchPage = () => import("./pages/Search");
Combine your configuration and implementations using createRouter.
import { createRouter } from "@werkbank/router";
const router = createRouter(routeConfig, {
dashboard: {
component: Dashboard,
// Simple inline preload
preload: () => {
console.log("Dashboard preloading...");
},
},
userProfile: {
component: UserProfile,
preload: preloadUser,
},
search: {
component: SearchPage, // Automatically handled as lazy
loading: () => <div>Loading Search...</div>,
},
settings: {
component: ({ children }) => <div><h1>Settings</h1>{children}</div>,
children: {
profile: {
component: () => <div>Profile Settings</div>,
},
},
},
});
Use createLinks to generate a helper object for creating URLs. This ensures you can never generate a broken link or miss a required parameter.
import { createLinks } from "@werkbank/router";
const links = createLinks(routeConfig);
// Usage
const dashboardUrl = links.dashboard();
// -> "/dashboard"
const userUrl = links.userProfile({ params: ["user_123"] });
// -> "/users/user_123"
const searchUrl = links.search({
searchParams: { q: "react", page: 1 }
});
// -> "/search?q=react&page=1"
Render the Router component at the root of your application, passing the created router configuration.
import { Router } from "@werkbank/router";
import { createRoot } from "react-dom/client";
const root = createRoot(document.getElementById("root"));
root.render(
<Router config={router} />
);
Finally, use the Link component and useNavigation hook in your application.
The Link component is fully type-safe based on your router configuration.
import { Link } from "@werkbank/router";
// Type-safe navigation
<Link
to={links.userProfile({ params: ["user_123"] })}
>
View Profile
</Link>
Use the useNavigation hook to navigate imperatively.
import { useNavigation } from "@werkbank/router";
const MyComponent = () => {
const { navigate } = useNavigation();
const handleLogin = () => {
navigate(links.dashboard());
};
return <button onClick={handleLogin}>Login</button>;
};