Router
RouterModule.forRoot(routes) provides a signal-backed Router: the matched route is a computed, so outlets and active-link styling re-render on navigation for free.
Setup
import type { Route } from '@preact-mvc/core';
import { TodoModule } from './todos/todo.module';
import { HomeView } from './home.view';
import { UserView } from './user.view';
import { NotFoundView } from './not-found.view';
export const routes: Route[] = [
{ path: '/', component: HomeView },
{ path: '/todos', module: TodoModule }, // renders its bootstrap view inside TodoModule's injector
{ path: '/user/:name', component: UserView }, // :param segments capture + decode
{ path: '*', component: NotFoundView }, // wildcard fallback
];@Module({
imports: [RouterModule.forRoot(routes)],
bootstrap: [AppShell],
})
export class AppModule {}Outlet and links
@Component()
export class AppShell extends View {
private router = inject(Router);
render() {
return (
<div class="shell">
<nav>
<Link to="/" class={this.router.isActive('/') ? 'active' : ''}>Home</Link>
<Link to="/todos">Todos</Link>
</nav>
<main>
<RouterOutlet />
</main>
</div>
);
}
}<Link> renders a real anchor (base-prefixed href — middle-click works) and intercepts plain clicks for SPA navigation. Router.navigate(to) does the same imperatively;popstate is wired for back/forward.
Params
@Component()
export class UserView extends View {
private router = inject(Router);
render() {
// params is a computed — updates if the route rematches.
return <p>Hello, {this.router.params.value.name}</p>;
}
}Module routes = scoped injectors
A route with module mounts that module's bootstrap view inside the module's own injector (cached per parent — the same injector serves repeat visits). Feature services scoped to the module persist while you navigate inside it and are disposed when the injector is released.
Nested routes
children match by prefix: the URL resolves as a chain outermost → innermost, and each <RouterOutlet/> renders one level by depth — a routed view can host its own outlet and stay mounted while children swap underneath it.
export const routes: Route[] = [
{
path: '/settings',
component: SettingsShell, // stays mounted — renders <RouterOutlet/>
children: [
{ path: 'general', component: GeneralView }, // /settings/general
{ path: 'security', component: SecurityView }, // /settings/security
],
},
];@Component()
export class SettingsShell extends View {
render() {
return (
<section>
<h2>Settings</h2>
<nav>
<Link to="/settings/general">General</Link>
<Link to="/settings/security">Security</Link>
</nav>
{/* depth-1 outlet: renders whichever child matched */}
<RouterOutlet />
</section>
);
}
}router.matches exposes the whole chain; router.params merges params from every level (inner wins on collisions), so a child sees its parent's params too. A child with module nests its injector under the level that mounted it. A parent with children still matches when the URL ends at it (/settings renders the shell alone); static children outrank :param siblings by config order.
preact-router underneath
The router rides preact-router: the outlet mounts a bridge that claims every URL, so navigation is one operation no matter who initiates it — navigate() goes through route() and updates any embedded preact-router <Router>, while foreign route() calls and plain <a href="/…"> clicks drive our router back through the bridge. If app code hosts its own preact-router subtree, both sides stay in sync.
Mounted apps: basePath
The router derives its mount prefix from document.baseURI at construction — an app served from /docs/demo/ automatically matches routes against the in-app path (/users, not /docs/demo/users) and prefixes it back onto Link hrefs and navigate(). No config needed for subpath hosting; override explicitly for edge cases:
RouterModule.forRoot(routes, { basePath: '/app/' }) // Angular's APP_BASE_HREF| API | With base /demo |
|---|---|
router.url.value | '/users' — always the in-app path |
router.href('/users') | '/demo/users' — what anchors emit |
router.navigate('/users') | pushState → '/demo/users' |
ROUTER_BASE token | Inject the configured base string anywhere |
History routing still needs a host that serves the app shell for unknown paths — or per-route files. See Hosting & Pages for the trade-offs on static hosts.