Routing
Client-side navigation with dynamic params, nested layouts, middleware, and view transitions.
Basic Setup
Define your routes and render the Router component. Each route maps a URL path to a component:
import { mount } from 'what-framework';
import { Router, defineRoutes } from 'what-framework/router';
import Home from './pages/Home';
import About from './pages/About';
import NotFound from './pages/NotFound';
const routes = defineRoutes({
'/': Home,
'/about': About,
});
mount(
<Router routes={routes} fallback={NotFound} />,
'#app'
);
defineRoutes accepts a plain object where keys are URL paths and values are components. The fallback prop renders when no route matches (404).
Navigating with Link
Use Link for client-side navigation. It intercepts clicks, updates the URL via history.pushState, and re-renders the router without a full page reload:
import { Link } from 'what-framework/router';
function Nav() {
return (
<nav>
<Link href="/">Home</Link>
<Link href="/about">About</Link>
<Link href="/blog">Blog</Link>
</nav>
);
}
Link automatically adds an active class when the current URL matches (or starts with) the href. For exact matching, it also adds exact-active:
// Customize active class names
<Link href="/blog" activeClass="nav-active" exactActiveClass="nav-exact">
Blog
</Link>
// Replace history entry instead of pushing
<Link href="/login" replace>Sign In</Link>
Programmatic Navigation
Use navigate() to change routes from event handlers or effects:
import { navigate } from 'what-framework/router';
function LoginForm() {
async function handleSubmit(e) {
e.preventDefault();
await login(email, password);
navigate('/dashboard');
}
return <form onSubmit={handleSubmit}>...</form>;
}
// Options
navigate('/settings', { replace: true }); // Replace history entry
navigate('/profile', { state: { from: 'nav' } }); // Pass state
navigate('/page', { transition: false }); // Skip view transition
The route Object
Access the current route state from anywhere in your app using the reactive route object:
import { route } from 'what-framework/router';
// All properties are reactive getters
route.path // "/users/42"
route.params // { id: "42" }
route.query // { tab: "posts" }
route.hash // "#section"
route.isNavigating // true during navigation
No Parentheses Needed
route.path, route.params, etc. are property getters that internally read signals. You access them like normal properties — no () call needed. They are still reactive and will trigger re-renders when they change.
Dynamic Params
Use :param syntax in route paths to capture dynamic segments. The matched values are passed to your component as params:
const routes = defineRoutes({
'/': Home,
'/users/:id': UserProfile,
'/posts/:slug': BlogPost,
'/files/*': FileViewer, // Catch-all
'/docs/[...path]': DocsPage, // Named catch-all
});
function UserProfile({ params }) {
return <h1>User #{params.id}</h1>;
}
function DocsPage({ params }) {
return <p>Path: {params.path}</p>; // "guides/getting-started"
}
File-based syntax like [id] and [...path] also works — it's converted to :id and catch-all internally.
Nested Layouts
Wrap groups of routes in shared layouts. The layout component receives the matched route's element as children:
import { nestedRoutes } from 'what-framework/router';
function DashboardLayout({ children }) {
return (
<div class="dashboard">
<aside>
<Link href="/dashboard">Overview</Link>
<Link href="/dashboard/settings">Settings</Link>
</aside>
<main>{children}</main>
</div>
);
}
const routes = [
{ path: '/', component: Home },
...nestedRoutes('/dashboard', [
{ path: '/', component: DashboardHome },
{ path: '/settings', component: Settings },
{ path: '/users/:id', component: UserDetail },
], { layout: DashboardLayout }),
];
Route Guards & Middleware
Protect routes with middleware functions that run before the route renders. Middleware can allow, reject, or redirect:
import { guard } from 'what-framework/router';
// Sync guard — redirect if not authenticated
const requireAuth = guard(
() => isLoggedIn(),
'/login' // Redirect path on failure
);
const ProtectedDashboard = requireAuth(Dashboard);
// Or use middleware in route config
const routes = defineRoutes({
'/dashboard': {
component: Dashboard,
middleware: [
({ path, params }) => {
if (!isLoggedIn()) return '/login'; // Redirect
if (!isAdmin()) return false; // Block (403)
return true; // Allow
}
],
},
});
For async checks (like verifying a session with the server), use asyncGuard:
import { asyncGuard } from 'what-framework/router';
const requireSession = asyncGuard(
async () => {
const res = await fetch('/api/session');
return res.ok;
},
{ fallback: '/login', loading: Spinner }
);
Route Groups
Group routes together to share a layout or middleware without affecting URL structure. The group name in parentheses is stripped from the URL:
import { routeGroup } from 'what-framework/router';
const routes = [
...routeGroup('marketing', [
{ path: '/', component: Home },
{ path: '/about', component: About },
{ path: '/pricing', component: Pricing },
], { layout: MarketingLayout }),
...routeGroup('app', [
{ path: '/dashboard', component: Dashboard },
{ path: '/settings', component: Settings },
], { layout: AppLayout, middleware: [requireAuth] }),
];
Loading & Error States
Specify per-route loading and error components:
const routes = defineRoutes({
'/dashboard': {
component: Dashboard,
loading: DashboardSkeleton, // Shown during navigation
error: DashboardError, // Shown on render error
},
});
The loading component renders while route.isNavigating is true. The error component is wrapped in an ErrorBoundary that catches exceptions during rendering.
View Transitions
What Router uses the browser's View Transitions API by default for smooth page transitions. No configuration needed — just add CSS:
/* Fade in/out by default */
::view-transition-old(root) {
animation: fade-out 150ms ease-out;
}
::view-transition-new(root) {
animation: fade-in 150ms ease-in;
}
@keyframes fade-out { to { opacity: 0; } }
@keyframes fade-in { from { opacity: 0; } }
Name specific elements for independent transitions:
import { viewTransitionName } from 'what-framework/router';
function Card({ id, title }) {
return (
<div {...viewTransitionName(`card-${id}`)}>
<h2>{title}</h2>
</div>
);
}
Scroll Restoration
Enable automatic scroll position saving and restoration:
import { enableScrollRestoration } from 'what-framework/router';
enableScrollRestoration();
// Now back/forward navigation restores scroll position
// New navigations scroll to top
// Hash links scroll to the target element
Prefetching
Link components prefetch routes on hover by default. You can also prefetch programmatically:
import { prefetch } from 'what-framework/router';
// Prefetch a route's assets
prefetch('/dashboard');
// Disable prefetch on a specific Link
<Link href="/heavy-page" prefetch={false}>Heavy Page</Link>
useRoute Hook
Access reactive route state inside any component with the useRoute hook. Returns computed signals for each property:
import { useRoute } from 'what-framework/router';
function Breadcrumbs() {
const { path, params, query, navigate } = useRoute();
return (
<nav class="breadcrumbs">
<span>You are at: {path()}</span>
</nav>
);
}
route vs useRoute
The global route object uses property getters (route.path) while useRoute() returns computed signals (path()). Both are reactive. Use route for quick access, useRoute when you want to destructure or pass around individual properties.