@doeixd/combi-router
v0.0.4
Published
A router based on parser combinators
Downloads
4
Maintainers
Readme
Combi-Router 🛤️
A composable, type-safe router built on my parser combinator library Combi Parse that thinks in trees. Routes are defined functionally and composed by reference, creating natural hierarchies that mirror your application structure.
📦 Installation
npm install @doeixd/combi-router @doeixd/combi-parse zodCombi-Router is built on @doeixd/combi-parse for robust URL parsing and uses zod for powerful, type-safe parameter validation.
✨ Key Features
🔗 Type-Safe & Composable
Build routes functionally and compose them by reference for perfect type safety and effortless refactoring.
🌳 Hierarchical & Introspective
Routes create natural trees that mirror your app's structure, with built-in utilities to analyze the hierarchy.
⚡ Powerful Parallel Data Loading
Automatically run data loaders for all nested routes in parallel (not sequentially), achieving 2-3x faster page loads. Advanced resource system with Suspense, caching, retries, and invalidation.
🧩 Composable Layer Architecture
Build your ideal router by mixing and matching feature layers (data, performance, dev tools) or creating your own.
🛡️ Advanced Navigation & Guards
Navigate with detailed results, cancellation support, and robust, type-safe route guards for fine-grained access control.
🎨 Enhanced View Layer
Universal template support with morphdom integration, true nested routing with outlets, and support for any templating system.
🔎 Integrated SEO & Head Management
Dynamically manage document head tags, including titles, meta descriptions, and social cards, directly from your route definitions.
✂️ Tree-Shakeable & Modular
A modular design ensures you only bundle the features you use, keeping your app lean and fast.
🛠️ Superior Developer Experience
Get dev-mode warnings, advanced debugging utilities, and detailed route analysis right out of the box.
🚀 Quick Start
Let's start simple and build up your understanding step by step.
Understanding Routes
A route in Combi-Router is a blueprint that describes a URL's structure and behavior.
import { route, path } from '@doeixd/combi-router';
// This route matches the exact path "/users"
export const usersRoute = route(path('users'));The route() function creates a new route from matchers. Matchers are small building blocks that each handle one part of a URL.
Why export routes? Routes are first-class objects you'll reference throughout your app for navigation, so treating them as exportable values makes them reusable and type-safe.
Basic Matchers
import { route, path, param } from '@doeixd/combi-router';
import { z } from 'zod';
// Static path segment
export const aboutRoute = route(path('about')); // matches "/about"
// Dynamic parameter with validation
export const userRoute = route(
path('users'),
param('id', z.number()) // matches "/users/123" -> params.id is a number
);Why validation? URLs are just strings. By validating during route matching, you catch errors early and get proper TypeScript types for your parameters.
Building Route Trees
The real power comes from composing routes by reference. Instead of redefining common parts, you extend existing routes:
import { extend } from '@doeixd/combi-router';
// Base route
export const dashboardRoute = route(path('dashboard'));
// Extend the base route
export const usersRoute = extend(dashboardRoute, path('users'));
export const userRoute = extend(usersRoute, param('id', z.number()));
// This creates a natural tree:
// /dashboard <- dashboardRoute
// /dashboard/users <- usersRoute
// /dashboard/users/123 <- userRouteWhy extend? When you change the base route (e.g., to /admin), all extended routes automatically update. Your route structure mirrors your application structure.
Adding Behavior with Higher-Order Functions
Enhance routes with additional behavior using pipe() and higher-order functions:
import { meta, loader, layout, pipe } from '@doeixd/combi-router';
export const enhancedUserRoute = pipe(
userRoute,
meta({ title: 'User Profile' }),
loader(async ({ params }) => {
const user = await fetchUser(params.id);
return { user };
}),
layout(ProfileLayout)
);Why higher-order functions? They're composable and reusable. You can create your own enhancers and mix them with built-in ones.
Creating the Router
Once you have routes, create a router instance from an array of all your routes:
import { createRouter } from '@doeixd/combi-router';
const router = createRouter([
dashboardRoute,
usersRoute,
enhancedUserRoute
]);
// Reference-based navigation with detailed results
const result = await router.navigate(enhancedUserRoute, { id: 123 });
if (result.success) {
console.log('Navigation successful');
} else {
console.error('Navigation failed:', result.error);
}
// Simple navigation for backward compatibility
const success = await router.navigateSimple(enhancedUserRoute, { id: 123 });
// Type-safe URL building
const userUrl = router.build(enhancedUserRoute, { id: 123 }); // "/dashboard/users/123"Why route references? Using actual route objects instead of string names provides perfect type inference and makes refactoring safe. TypeScript knows exactly what parameters each route needs.
🏗️ Core Concepts
Route Building Improvements
Route Introspection Utilities
Routes now provide powerful introspection capabilities to analyze their structure:
import { route, extend, path, param } from '@doeixd/combi-router';
import { z } from 'zod';
const dashboardRoute = route(path('dashboard'));
const usersRoute = extend(dashboardRoute, path('users'));
const userRoute = extend(usersRoute, param('id', z.number()));
// Analyze route structure
console.log(userRoute.depth); // 2 (dashboard -> users -> user)
console.log(userRoute.ancestors); // [dashboardRoute, usersRoute]
console.log(userRoute.staticPath); // "/dashboard/users"
console.log(userRoute.paramNames); // ["id"]
console.log(userRoute.isDynamic); // true
console.log(userRoute.routeChain); // [dashboardRoute, usersRoute, userRoute]Route Validation at Creation Time
Routes are now validated when created, catching common configuration errors early:
import { RouteValidationError } from '@doeixd/combi-router';
try {
// This will throw if there are duplicate parameter names
const problematicRoute = extend(
route(param('id', z.string())),
param('id', z.number()) // Error: Duplicate parameter name 'id'
);
} catch (error) {
if (error instanceof RouteValidationError) {
console.error('Route configuration error:', error.message);
}
}Parent-Child Relationships
Routes maintain explicit parent-child relationships for better debugging and tooling:
console.log(userRoute.parent === usersRoute); // true
console.log(usersRoute.parent === dashboardRoute); // true
console.log(dashboardRoute.parent); // null (root route)
// Walk up the hierarchy
let current = userRoute;
while (current) {
console.log(current.staticPath);
current = current.parent;
}
// Output: "/dashboard/users", "/dashboard", "/"Route Matchers
Matchers are the building blocks of routes. Each matcher handles one aspect of URL parsing:
// Path segments
path('users') // matches "/users"
path.optional('category') // matches "/category" or ""
path.wildcard('segments') // matches "/any/number/of/segments"
// Parameters with validation
param('id', z.number()) // matches "/123" and validates as number
param('slug', z.string().min(3)) // matches "/hello" with minimum length
// Query parameters
query('page', z.number().default(1)) // matches "?page=5"
query.optional('search', z.string()) // matches "?search=term"
// Other components
end // ensures no remaining path segments
// subdomain(...) and hash(...) can be added with similar patternsRoute Composition
Routes are composed functionally using extend():
export const apiRoute = route(path('api'), path('v1'));
export const usersRoute = extend(apiRoute, path('users'));
export const userRoute = extend(usersRoute, param('id', z.number()));
// userRoute now matches /api/v1/users/123Parameters from parent routes are automatically inherited and merged into a single params object.
Parallel Data Loading
Combi-Router automatically executes loaders for all nested routes in parallel, not sequentially. This is a key performance feature that makes deeply nested routes load 2-3x faster.
// Example: Three-level nested route with loaders
const orgRoute = pipe(
route(path('org'), param('orgId', z.string())),
loader(async ({ params }) => {
// Fetches organization data (500ms)
return { org: await fetchOrg(params.orgId) };
})
);
const teamRoute = pipe(
extend(orgRoute, path('team'), param('teamId', z.string())),
loader(async ({ params }) => {
// Fetches team data (400ms)
return { team: await fetchTeam(params.teamId) };
})
);
const memberRoute = pipe(
extend(teamRoute, path('member'), param('memberId', z.string())),
loader(async ({ params }) => {
// Fetches member data (300ms)
return { member: await fetchMember(params.memberId) };
})
);
// When navigating to /org/1/team/2/member/3:
// ✅ All three loaders execute simultaneously
// ✅ Total load time: 500ms (the slowest loader)
// ❌ Without parallel loading: 1200ms (500+400+300)Why it matters: Traditional routers often load data sequentially, causing waterfalls. Combi-Router's parallel loading ensures optimal performance by default, without any configuration needed.
Higher-Order Route Enhancers
Enhance routes with additional functionality:
import { pipe, meta, loader, guard, cache, lazy } from '@doeixd/combi-router';
export const userRoute = pipe(
route(path('users'), param('id', z.number())),
meta({ title: (params) => `User ${params.id}` }),
loader(async ({ params }) => ({ user: await fetchUser(params.id) })),
guard(async () => await isAuthenticated() || '/login'),
cache({ ttl: 5 * 60 * 1000 }), // Cache for 5 minutes
lazy(() => import('./UserProfile'))
);🔧 Modular Architecture
Combi-Router now features a modular architecture optimized for tree-shaking and selective feature adoption.
Import Paths
// Core routing functionality (always included)
import { route, extend, createRouter } from '@doeixd/combi-router';
// Enhanced view layer with morphdom and template support
import {
createEnhancedViewLayer,
enhancedView,
lazyView,
conditionalView
} from '@doeixd/combi-router/enhanced-view';
// Advanced data loading and caching
import { createAdvancedResource, resourceState } from '@doeixd/combi-router/data';
// Production features and optimizations
import {
PerformanceManager,
ScrollRestorationManager,
TransitionManager
} from '@doeixd/combi-router/features';
// Development tools and debugging
import {
createWarningSystem,
analyzeRoutes,
DebugUtils
} from '@doeixd/combi-router/dev';
// Framework-agnostic utilities
import {
createLink,
createActiveLink,
createOutlet
} from '@doeixd/combi-router/utils';Module Breakdown
Core Module (@doeixd/combi-router)
Essential routing functionality including route definition, matching, navigation, and basic data loading.
import {
route, extend, path, param, query,
createRouter, pipe, meta, loader, guard
} from '@doeixd/combi-router';Data Module (@doeixd/combi-router/data)
Advanced resource management with caching, retry logic, and global state management.
import {
createAdvancedResource,
resourceState,
globalCache
} from '@doeixd/combi-router/data';
// Enhanced resource with retry and caching
const userResource = createAdvancedResource(
() => api.fetchUser(userId),
{
retry: { attempts: 3 },
cache: { ttl: 300000, invalidateOn: ['user'] },
staleTime: 60000,
backgroundRefetch: true
}
);Features Module (@doeixd/combi-router/features)
Production-ready features for performance optimization and user experience.
import {
PerformanceManager,
ScrollRestorationManager,
TransitionManager,
CodeSplittingManager
} from '@doeixd/combi-router/features';
// Initialize performance monitoring
const performanceManager = new PerformanceManager({
prefetchOnHover: true,
prefetchViewport: true,
enablePerformanceMonitoring: true,
connectionAware: true
});Dev Module (@doeixd/combi-router/dev)
Development tools for debugging and route analysis.
import {
createWarningSystem,
analyzeRoutes,
DebugUtils,
ConflictDetector
} from '@doeixd/combi-router/dev';
// Create warning system for development
const warningSystem = createWarningSystem(router, {
runtimeWarnings: true,
performanceWarnings: true
});
// Quick route analysis
analyzeRoutes(router);Utils Module (@doeixd/combi-router/utils)
Framework-agnostic utilities for DOM integration.
import {
createLink,
createActiveLink,
createOutlet,
createMatcher,
createRouterStore
} from '@doeixd/combi-router/utils';Bundle Size Optimization
The modular architecture enables significant bundle size optimization:
// Minimal bundle - only core routing
import { route, extend, createRouter } from '@doeixd/combi-router';
// With advanced resources
import { createAdvancedResource } from '@doeixd/combi-router/data';
// With production features
import { PerformanceManager } from '@doeixd/combi-router/features';
// Development tools (excluded in production)
import { createWarningSystem } from '@doeixd/combi-router/dev';
// (dev only)📊 Enhanced Resource System
The new resource system provides production-ready data loading with advanced features.
Basic Resources with Parallel Loading
import { createResource } from '@doeixd/combi-router';
// Simple suspense-based resource with automatic parallel fetching
const userRoute = pipe(
route(path('users'), param('id', z.number())),
loader(({ params }) => ({
// These resources load in parallel automatically
user: createResource(() => fetchUser(params.id)),
posts: createResource(() => fetchUserPosts(params.id))
}))
);
// In your component
function UserProfile() {
const { user, posts } = router.currentMatch.data;
// These will suspend until data is ready
const userData = user.read();
const postsData = posts.read();
return <div>...</div>;
}Advanced Resources
import { createAdvancedResource, resourceState } from '@doeixd/combi-router/data';
// Enhanced resource with all features
const userResource = createAdvancedResource(
() => api.fetchUser(userId),
{
// Retry configuration with exponential backoff
retry: {
attempts: 3,
delay: (attempt) => Math.min(1000 * Math.pow(2, attempt - 1), 10000),
shouldRetry: (error) => error.status >= 500,
onRetry: (error, attempt) => console.log(`Retry ${attempt}:`, error)
},
// Caching with tags for invalidation
cache: {
ttl: 300000, // 5 minutes
invalidateOn: ['user', 'profile'],
priority: 'high'
},
// Stale-while-revalidate behavior
staleTime: 60000, // 1 minute
backgroundRefetch: true
}
);
// Check state without suspending
if (userResource.isLoading) {
console.log('Loading user...');
}
// Non-suspending peek at cached data
const cachedUser = userResource.peek();
if (cachedUser) {
console.log('Cached user:', cachedUser);
}
// Force refresh
await userResource.refetch();
// Invalidate resource
userResource.invalidate();Cache Management
import { resourceState } from '@doeixd/combi-router/data';
// Global resource state monitoring
const globalState = resourceState.getGlobalState();
console.log('Loading resources:', globalState.loadingCount);
// Event system for observability
const unsubscribe = resourceState.onEvent((event) => {
switch (event.type) {
case 'fetch-start':
console.log('Started loading:', event.resource);
break;
case 'fetch-success':
console.log('Loaded successfully:', event.data);
break;
case 'fetch-error':
console.error('Loading failed:', event.error);
break;
case 'retry':
console.log(`Retry attempt ${event.attempt}:`, event.error);
break;
}
});
// Cache invalidation by tags
resourceState.invalidateByTags(['user', 'profile']);🚀 Performance Features
Intelligent Prefetching
import { PerformanceManager } from '@doeixd/combi-router/features';
const performanceManager = new PerformanceManager({
// Prefetch on hover with delay
prefetchOnHover: true,
// Prefetch when links enter viewport
prefetchViewport: true,
// Adjust behavior based on connection
connectionAware: true,
// Monitor performance metrics
enablePerformanceMonitoring: true,
// Preload critical routes immediately
preloadCriticalRoutes: ['dashboard', 'user-profile'],
// Memory management
memoryManagement: {
enabled: true,
maxCacheSize: 50,
maxCacheAge: 30 * 60 * 1000,
cleanupInterval: 5 * 60 * 1000
}
});
// Setup hover prefetching for a link
const cleanup = performanceManager.setupHoverPrefetch(linkElement, 'user-route');
// Setup viewport prefetching
const cleanupViewport = performanceManager.setupViewportPrefetch(linkElement, 'user-route');
// Get performance report
const report = performanceManager.getPerformanceReport();
console.log('Prefetch hit rate:', report.prefetchHitRate);Scroll Restoration
import { ScrollRestorationManager } from '@doeixd/combi-router/features';
const scrollManager = new ScrollRestorationManager({
enabled: true,
restoreOnBack: true,
restoreOnForward: true,
saveScrollState: true,
smoothScrolling: true,
scrollBehavior: 'smooth',
debounceTime: 100,
// Advanced configuration
customScrollContainer: '#main-content',
excludeRoutes: ['modal-routes'],
persistScrollState: true
});
// Manual scroll position management
scrollManager.saveScrollPosition(routeId);
scrollManager.restoreScrollPosition(routeId);
scrollManager.scrollToTop();
scrollManager.scrollToElement('#section');Advanced Transitions
import { TransitionManager } from '@doeixd/combi-router/features';
const transitionManager = new TransitionManager({
enabled: true,
duration: 300,
easing: 'ease-in-out',
type: 'fade',
// Per-route transition configuration
routeTransitions: {
'user-profile': { type: 'slide-left', duration: 400 },
'settings': { type: 'fade', duration: 200 }
},
// Custom transition classes
transitionClasses: {
enter: 'page-enter',
enterActive: 'page-enter-active',
exit: 'page-exit',
exitActive: 'page-exit-active'
}
});
// Manual transition control
await transitionManager.performTransition(fromRoute, toRoute, {
direction: 'forward',
customData: { userId: 123 }
});🛠️ Development Experience
Development Warnings
import { createWarningSystem, analyzeRoutes } from '@doeixd/combi-router/dev';
// Create comprehensive warning system
const warningSystem = createWarningSystem(router, {
runtimeWarnings: true,
staticWarnings: true,
performanceWarnings: true,
severityFilter: ['warning', 'error']
});
// Quick route analysis
analyzeRoutes(router);
// Get warnings programmatically
const warnings = warningSystem.getWarnings();
const conflictWarnings = warningSystem.getWarningsByType('conflicting-routes');
const errorWarnings = warningSystem.getWarningsBySeverity('error');Debugging Tools
import { DebugUtils } from '@doeixd/combi-router/dev';
// Route structure debugging
DebugUtils.logRouteTree(router);
DebugUtils.analyzeRoutePerformance(router);
DebugUtils.checkRouteConflicts(router);
// Navigation debugging
DebugUtils.enableNavigationLogging(router);
DebugUtils.logMatchDetails(currentMatch);
// Performance debugging
DebugUtils.enablePerformanceMonitoring(router);
const metrics = DebugUtils.getPerformanceMetrics();Enhanced Error Handling
import { NavigationErrorType } from '@doeixd/combi-router';
const result = await router.navigate(userRoute, { id: 123 });
if (!result.success) {
switch (result.error?.type) {
case NavigationErrorType.RouteNotFound:
console.error('Route not found');
break;
case NavigationErrorType.GuardRejected:
console.error('Navigation blocked:', result.error.message);
break;
case NavigationErrorType.LoaderFailed:
console.error('Data loading failed:', result.error.originalError);
break;
case NavigationErrorType.ValidationFailed:
console.error('Parameter validation failed');
break;
case NavigationErrorType.Cancelled:
console.log('Navigation was cancelled');
break;
}
}🔄 Migration Guide
From v1.x to v2.x
Modular Imports
Before:
import { createRouter, createResource, createLink } from '@doeixd/combi-router';After:
// Core functionality
import { createRouter } from '@doeixd/combi-router';
// Advanced resources (optional)
import { createAdvancedResource } from '@doeixd/combi-router/data';
// Utilities (optional)
import { createLink } from '@doeixd/combi-router/utils';Enhanced Resources
Before:
const resource = createResource(() => fetchUser(id));After:
// Simple resource (same API)
const resource = createResource(() => fetchUser(id));
// Or enhanced resource with more features
const resource = createAdvancedResource(
() => fetchUser(id),
{
retry: { attempts: 3 },
cache: { ttl: 300000 },
staleTime: 60000
}
);Navigation API
The navigation API is fully backward compatible. Enhanced error handling is opt-in:
// Old way (still works)
const success = await router.navigateSimple(route, params);
// New way (detailed error information)
const result = await router.navigate(route, params);
if (result.success) {
// Handle success
} else {
// Handle specific error types
}🎨 Enhanced View Layer
The Enhanced View Layer extends Combi-Router with advanced DOM rendering capabilities, efficient updates through morphdom, and true nested routing support.
Universal Template Support
Work with any templating system - lit-html, uhtml, Handlebars, or plain strings:
import { createEnhancedViewLayer, enhancedView } from '@doeixd/combi-router/enhanced-view';
import { html } from 'lit-html';
// Using lit-html templates
const userRoute = pipe(
route(path('user'), param('id', z.string()), end),
enhancedView(({ match }) => html`
<div class="user-profile">
<h1>${match.data.user.name}</h1>
<p>Email: ${match.data.user.email}</p>
</div>
`)
);
// Using custom template engines
import Handlebars from 'handlebars';
const template = Handlebars.compile(`
<div class="product">
<h2>{{name}}</h2>
<p>Price: \${{price}}</p>
</div>
`);
const productRoute = pipe(
route(path('product'), param('id', z.string()), end),
enhancedView(({ match }) => ({
html: template(match.data.product)
}))
);
// Configure the router with enhanced view layer
const router = createLayeredRouter(routes)
(createCoreNavigationLayer())
(createEnhancedViewLayer({
root: '#app',
useMorphdom: true,
templateRenderer: (result, container) => {
// Custom renderer for your template library
if (result._$litType$) {
litRender(result, container);
}
}
}))
();Morphdom Integration
Enable efficient DOM patching that preserves form state, focus, and scroll position:
import morphdom from 'morphdom';
import { setMorphdom } from '@doeixd/combi-router/enhanced-view';
// Provide morphdom implementation
setMorphdom(morphdom);
// Configure morphdom behavior
const router = createLayeredRouter(routes)
(createCoreNavigationLayer())
(createEnhancedViewLayer({
root: '#app',
useMorphdom: true,
morphdomOptions: {
onBeforeElUpdated: (fromEl, toEl) => {
// Preserve focus
if (fromEl === document.activeElement) {
return false;
}
// Preserve form values
if (fromEl.tagName === 'INPUT') {
toEl.value = fromEl.value;
}
return true;
},
onElUpdated: (el) => {
// Add animation classes
el.classList.add('updated');
setTimeout(() => el.classList.remove('updated'), 300);
}
}
}))
();True Nested Routing with Outlets
Leverage the hierarchical route structure for automatic nested view rendering:
// Parent route with outlet
const appRoute = pipe(
route(path('')),
enhancedView(() => html`
<div class="app">
<header>
<nav>
<a href="/">Home</a>
<a href="/dashboard">Dashboard</a>
</nav>
</header>
<!-- Child routes render here automatically -->
<main router-outlet></main>
</div>
`)
);
// Dashboard with its own nested outlet
const dashboardRoute = pipe(
extend(appRoute, path('dashboard')),
enhancedView(({ match }) => html`
<div class="dashboard">
<aside>
<a href="/dashboard/overview">Overview</a>
<a href="/dashboard/analytics">Analytics</a>
</aside>
<!-- Nested child routes render here -->
<section router-outlet router-outlet-parent="${match.route.id}">
</section>
</div>
`)
);
// Child routes automatically render in parent outlets
const overviewRoute = pipe(
extend(dashboardRoute, path('overview'), end),
enhancedView(() => html`
<div class="overview">
<h2>Dashboard Overview</h2>
<p>Your stats and metrics...</p>
</div>
`)
);Parallel Data Loading in Nested Routes
One of Combi-Router's most powerful features is automatic parallel data fetching for nested routes. When navigating to a deeply nested route, all loaders execute simultaneously, not sequentially.
How It Works
// Each route has its own loader
const workspaceRoute = pipe(
extend(appRoute, path('workspace'), param('workspaceId', z.string())),
loader(async ({ params }) => {
const workspace = await fetchWorkspace(params.workspaceId); // Takes 500ms
return { workspace };
})
);
const projectRoute = pipe(
extend(workspaceRoute, path('project'), param('projectId', z.string())),
loader(async ({ params }) => {
const project = await fetchProject(params.projectId); // Takes 400ms
return { project };
})
);
const taskRoute = pipe(
extend(projectRoute, path('task'), param('taskId', z.string())),
loader(async ({ params }) => {
const task = await fetchTask(params.taskId); // Takes 300ms
return { task };
})
);
// When navigating to /workspace/123/project/456/task/789:
// ALL three loaders start simultaneously!
// Total time: ~500ms (the longest loader), NOT 1200ms!Performance Impact
- Sequential Loading: 500ms + 400ms + 300ms = 1200ms ❌
- Parallel Loading: max(500ms, 400ms, 300ms) = 500ms ✅
This results in 2-3x faster page loads for deeply nested routes!
Configuration
const router = createLayeredRouter(routes)
(createCoreNavigationLayer())
(createLoaderLayer({
parallelLoading: true, // Enabled by default
loaderTimeout: 10000, // Timeout applies to each loader individually
}))
();Best Practices
// ✅ Good: Independent loaders using URL params
const teamRoute = pipe(
extend(orgRoute, path('team'), param('teamId', z.string())),
loader(async ({ params }) => {
// Uses teamId from URL, doesn't wait for parent data
const team = await fetchTeam(params.teamId);
return { team };
})
);
// ✅ Good: Access parent data after parallel loading
const projectView = enhancedView(({ match }) => {
// All data is available after parallel loading completes
const workspace = match.parent?.data?.workspace;
const project = match.data.project;
return html`
<h1>${workspace.name} / ${project.name}</h1>
`;
});Outlet Configuration
<!-- Basic outlet -->
<div router-outlet></div>
<!-- Outlet with specific parent route -->
<div router-outlet router-outlet-parent="42"></div>
<!-- Outlet with transitions -->
<div
router-outlet
router-outlet-enter="fade-in"
router-outlet-leave="fade-out"
router-outlet-duration="300">
</div>
<!-- Preserve scroll position -->
<div router-outlet router-outlet-preserve-scroll></div>Advanced View Functions
Lazy Loading Views
const route = pipe(
route(path('heavy'), end),
lazyView(
() => import('./heavy-view').then(m => m.default),
() => '<div>Loading...</div>' // Loading view while importing
)
);Conditional Views
const route = pipe(
route(path('profile'), param('id'), end),
conditionalView(
({ match }) => match.data.user.isAdmin,
({ match }) => html`<admin-dashboard user="${match.data.user}"></admin-dashboard>`,
({ match }) => html`<user-profile user="${match.data.user}"></user-profile>`
)
);Error Boundary Views
const route = pipe(
route(path('fragile'), end),
errorBoundaryView(
({ match }) => riskyRenderFunction(match),
(error) => html`
<div class="error">
<h2>Something went wrong</h2>
<p>${error.message}</p>
</div>
`
)
);Composed Views
const route = pipe(
route(path('complex'), end),
composeViews({
header: ({ match }) => html`<header>${match.data.title}</header>`,
sidebar: () => html`<nav>Menu items...</nav>`,
content: ({ match }) => html`<main>${match.data.content}</main>`
}, (parts) => html`
<div class="layout">
${parts.header}
<div class="body">
${parts.sidebar}
${parts.content}
</div>
</div>
`)
);Cached Views
const route = pipe(
route(path('expensive'), param('id'), end),
cachedView(
({ match }) => expensiveRender(match.data),
({ match }) => `cache-${match.params.id}`, // Cache key
60000 // Cache for 1 minute
)
);Configuration Options
interface EnhancedViewLayerConfig {
// Root element for rendering (required)
root: HTMLElement | string;
// Enable morphdom for efficient updates
useMorphdom?: boolean;
// Morphdom configuration
morphdomOptions?: MorphdomOptions;
// Custom template renderer for your library
templateRenderer?: (result: any, container: HTMLElement) => void;
// State views
loadingView?: () => any;
errorView?: (error: NavigationError) => any;
notFoundView?: () => any;
// Nested routing support
enableOutlets?: boolean;
outletAttribute?: string; // default: 'router-outlet'
}Why Enhanced View Layer?
The enhanced view layer solves common SPA rendering challenges:
- No Template Lock-in: Use lit-html, uhtml, Handlebars, or any other template system
- Efficient Updates: Morphdom ensures only changed DOM nodes are updated
- True Nested Routing: Hierarchical routes automatically manage nested views through outlets
- Progressive Enhancement: Start with simple string templates, upgrade to advanced features as needed
- Performance Optimized: Built-in caching, lazy loading, and smart update strategies
- Developer Friendly: Intuitive outlet system mirrors your route hierarchy
Enhanced View Layer API Reference
Core Functions
createEnhancedViewLayer(config)
Creates an enhanced view layer with morphdom support and nested routing.
function createEnhancedViewLayer(config: EnhancedViewLayerConfig): RouterLayer
interface EnhancedViewLayerConfig {
root: HTMLElement | string; // Root element for rendering (required)
useMorphdom?: boolean; // Enable morphdom for efficient updates
morphdomOptions?: MorphdomOptions; // Morphdom configuration
templateRenderer?: (result: TemplateResult, container: HTMLElement) => void;
loadingView?: () => string | Node | TemplateResult;
errorView?: (error: NavigationError) => string | Node | TemplateResult;
notFoundView?: () => string | Node | TemplateResult;
linkSelector?: string; // Custom link selector (default: 'a[href]')
disableLinkInterception?: boolean; // Disable automatic SPA navigation
enableOutlets?: boolean; // Enable nested routing outlets
outletAttribute?: string; // Outlet attribute name (default: 'router-outlet')
}enhancedView(factory)
Creates an enhanced view for a route supporting multiple template formats.
function enhancedView<TParams>(
factory: (context: ViewContext<TParams>) =>
string | Node | TemplateResult | HTMLTemplateResult | Promise<any>
): (route: Route<TParams>) => Route<TParams>
interface ViewContext<TParams> {
match: RouteMatch<TParams>; // Full route match with params, data, etc.
}htmlTemplate(html, options)
Creates an HTML template result with lifecycle hooks.
function htmlTemplate(
html: string,
options?: {
afterRender?: (element: HTMLElement) => void;
beforeRender?: () => void;
}
): HTMLTemplateResultlazyView(loader, loadingView)
Creates a lazily loaded view with optional loading state.
function lazyView<TParams>(
loader: () => Promise<EnhancedViewFactory<TParams>>,
loadingView?: EnhancedViewFactory<TParams>
): (route: Route<TParams>) => Route<TParams>conditionalView(condition, trueView, falseView)
Renders different views based on a condition.
function conditionalView<TParams>(
condition: (context: ViewContext<TParams>) => boolean,
trueView: EnhancedViewFactory<TParams>,
falseView: EnhancedViewFactory<TParams>
): (route: Route<TParams>) => Route<TParams>errorBoundaryView(view, errorView)
Wraps a view with error handling.
function errorBoundaryView<TParams>(
view: EnhancedViewFactory<TParams>,
errorView: (error: Error) => string | Node | TemplateResult
): (route: Route<TParams>) => Route<TParams>composeViews(parts, composer)
Composes multiple view parts into a single view.
function composeViews<TParams, TParts extends Record<string, any>>(
parts: { [K in keyof TParts]: EnhancedViewFactory<TParams> },
composer: (parts: TParts) => string | Node | TemplateResult
): (route: Route<TParams>) => Route<TParams>cachedView(factory, keyFn, ttl)
Caches rendered views for performance.
function cachedView<TParams>(
factory: EnhancedViewFactory<TParams>,
keyFn: (context: ViewContext<TParams>) => string,
ttl?: number // Time to live in milliseconds (default: 60000)
): (route: Route<TParams>) => Route<TParams>streamingView(generator)
Creates a streaming view that updates progressively.
function streamingView<TParams>(
generator: (context: ViewContext<TParams>) =>
AsyncGenerator<string | Node | TemplateResult>
): (route: Route<TParams>) => Route<TParams>Morphdom Integration
setMorphdom(morphdom)
Sets the morphdom implementation to use.
function setMorphdom(morphdom: MorphdomFn): void
type MorphdomFn = (
fromNode: Element,
toNode: Element | string,
options?: MorphdomOptions
) => ElementcreateMorphdomIntegration(options)
Creates a morphdom configuration with defaults.
function createMorphdomIntegration(options?: Partial<MorphdomOptions>): {
morphdom: MorphdomFn;
options: MorphdomOptions;
}
interface MorphdomOptions {
childrenOnly?: boolean;
onBeforeElUpdated?: (fromEl: Element, toEl: Element) => boolean;
onElUpdated?: (el: Element) => void;
onBeforeNodeAdded?: (node: Node) => Node | boolean;
onNodeAdded?: (node: Node) => void;
onBeforeNodeDiscarded?: (node: Node) => boolean;
onNodeDiscarded?: (node: Node) => void;
onBeforeElChildrenUpdated?: (fromEl: Element, toEl: Element) => boolean;
}Nested Routing
createNestedRouter(config)
Creates a nested router for parent-child route relationships.
function createNestedRouter(config: NestedRouterConfig): {
parent: Route<any>;
children: Route<any>[];
outlets: Map<string, RouterOutlet>;
findChildMatch: (match: RouteMatch | null) => RouteMatch | null;
renderChild: (match: RouteMatch | null, outlet?: HTMLElement) => void;
destroy: () => void;
}
interface NestedRouterConfig {
parentRoute: Route<any>;
childRoutes: Route<any>[];
outlet?: HTMLElement | string;
autoManageOutlet?: boolean;
}createRouterOutlet(router, config)
Creates a router outlet for automatic child route rendering.
function createRouterOutlet(
router: ComposableRouter<any>,
config: OutletConfig
): RouterOutlet & {
update: (match: RouteMatch | null) => void;
clear: () => void;
destroy: () => void;
}
interface OutletConfig {
element: HTMLElement;
parentRouteId?: number;
render?: (match: RouteMatch | null, element: HTMLElement) => void;
transition?: {
enter?: string;
leave?: string;
duration?: number;
};
preserveScroll?: boolean;
loadingView?: () => string | Node;
errorView?: (error: Error) => string | Node;
}setupAutoOutlets(router, routes, container, attribute)
Automatically discovers and sets up outlets in a container.
function setupAutoOutlets(
router: ComposableRouter<any>,
routes: Route<any>[],
container?: HTMLElement, // default: document.body
attribute?: string // default: 'router-outlet'
): () => void // Returns cleanup functionLayer Extensions
The enhanced view layer provides these methods on the router:
interface EnhancedViewLayerExtensions {
rerender(): void; // Re-render current view
getRootElement(): HTMLElement | null; // Get root element
updateConfig(config: Partial<EnhancedViewLayerConfig>): void;
registerOutlet(outlet: RouterOutlet): void; // Register outlet
unregisterOutlet(outlet: RouterOutlet): void; // Unregister outlet
morphUpdate(content: string | Node): void; // Force morphdom update
}
// Access layer extensions
const viewLayer = router.getLayer('EnhancedViewLayer');
viewLayer.rerender();
viewLayer.morphUpdate('<div>New content</div>');Type Definitions
// Template result types for various libraries
interface TemplateResult {
strings?: TemplateStringsArray;
values?: unknown[];
_$litType$?: number; // lit-html marker
[key: string]: any;
}
interface HTMLTemplateResult {
template?: HTMLTemplateElement;
render?: () => Node | string;
html?: string;
dom?: DocumentFragment;
}
// Enhanced view factory supporting multiple return types
type EnhancedViewFactory<TParams = any> = (
context: ViewContext<TParams>
) => string | Node | TemplateResult | HTMLTemplateResult | Promise<any>;
// Router outlet interface
interface RouterOutlet {
element: HTMLElement;
parentRouteId?: number;
render: (match: RouteMatch | null) => void;
}🗂️ Advanced Features
Document Head Management
The head management module provides comprehensive document head tag management with support for dynamic content, SEO optimization, and server-side rendering.
Basic Head Management
import { head, seoMeta } from '@doeixd/combi-router/features';
// Static head data
const aboutRoute = pipe(
route(path('about')),
head({
title: 'About Us',
meta: [
{ name: 'description', content: 'Learn more about our company' },
{ name: 'keywords', content: 'about, company, team' }
],
link: [
{ rel: 'canonical', href: 'https://example.com/about' }
]
})
);
// Dynamic head data based on route parameters
const userRoute = pipe(
route(path('users'), param('id', z.number())),
head(({ params }) => ({
title: `User Profile - ${params.id}`,
meta: [
{ name: 'description', content: `Profile page for user ${params.id}` }
]
}))
);SEO Optimization
// Complete SEO setup with Open Graph and Twitter Cards
const productRoute = pipe(
route(path('products'), param('id', z.number())),
head(({ params }) => ({
title: `Product ${params.id}`,
titleTemplate: 'Store | %s', // Results in: "Store | Product 123"
// Basic SEO
...seoMeta.basic({
description: `Amazing product ${params.id}`,
keywords: ['product', 'store', 'shopping'],
robots: 'index,follow'
}),
// Open Graph tags
...seoMeta.og({
title: `Product ${params.id}`,
description: 'The best product you will ever buy',
image: `https://example.com/products/${params.id}/image.jpg`,
url: `https://example.com/products/${params.id}`,
type: 'product'
}),
// Twitter Cards
...seoMeta.twitter({
card: 'summary_large_image',
title: `Product ${params.id}`,
description: 'An amazing product',
image: `https://example.com/products/${params.id}/twitter.jpg`
})
}))
);Advanced Features
// Scripts, styles, and HTML attributes
const dashboardRoute = pipe(
route(path('dashboard')),
head({
title: 'Dashboard',
script: [
{ src: 'https://analytics.example.com/track.js', async: true },
{ innerHTML: 'window.config = { theme: "dark" };' }
],
style: [
{ innerHTML: 'body { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); }' }
],
htmlAttrs: { lang: 'en', 'data-theme': 'dark' },
bodyAttrs: { class: 'dashboard dark-mode' }
})
);DOM Integration
import { HeadManager, resolveHeadData } from '@doeixd/combi-router/features';
// Initialize head manager
const headManager = new HeadManager(document);
// Update head tags on navigation
router.onNavigate((match) => {
if (match?.route._head) {
const resolvedHead = resolveHeadData(match.route._head, match);
headManager.apply(resolvedHead);
}
});For complete documentation, see Head Management Guide.
Navigation Improvements
NavigationResult with Detailed Error Handling
The navigate() method now returns a NavigationResult object with comprehensive information about the navigation attempt:
import { NavigationErrorType } from '@doeixd/combi-router';
const result = await router.navigate(userRoute, { id: 123 });
if (result.success) {
console.log('Navigation completed successfully');
console.log('Active match:', result.match);
} else {
// Handle different types of navigation errors
switch (result.error?.type) {
case NavigationErrorType.RouteNotFound:
console.error('Route not found');
break;
case NavigationErrorType.GuardRejected:
console.error('Navigation blocked by guard:', result.error.message);
break;
case NavigationErrorType.LoaderFailed:
console.error('Data loading failed:', result.error.originalError);
break;
case NavigationErrorType.ValidationFailed:
console.error('Parameter validation failed');
break;
case NavigationErrorType.Cancelled:
console.log('Navigation was cancelled');
break;
}
}Navigation Cancellation with NavigationController
Long-running navigations can now be cancelled, which is especially useful for preventing race conditions:
// Start a navigation and get a controller
const controller = router.currentNavigation;
if (controller) {
console.log('Navigating to:', controller.route);
// Cancel the navigation if needed
setTimeout(() => {
if (!controller.cancelled) {
controller.cancel();
console.log('Navigation cancelled');
}
}, 1000);
// Wait for the result
const result = await controller.promise;
if (result.cancelled) {
console.log('Navigation was cancelled');
}
}Backward Compatibility with navigateSimple()
For simple use cases, the navigateSimple() method provides the traditional boolean return value:
// Simple boolean result for straightforward cases
const success = await router.navigateSimple(userRoute, { id: 123 });
if (success) {
console.log('Navigation successful');
} else {
console.log('Navigation failed');
}
// Still get full details when needed
const detailedResult = await router.navigate(userRoute, { id: 123 });Typed Guards
Enhanced Guard Context and Type Safety
The new typedGuard() function provides better type safety and more context for route protection:
import { typedGuard, GuardContext } from '@doeixd/combi-router';
import { z } from 'zod';
// Define a route with parameters
const adminUserRoute = route(
path('admin'),
path('users'),
param('userId', z.string())
);
// Create a typed guard with full context access
const adminGuard = typedGuard<{ userId: string }>(({ params, to, from, searchParams }) => {
// Full type safety on params
const userId = params.userId; // TypeScript knows this is a string
// Access to route context
console.log('Navigating to:', to.url);
console.log('Coming from:', from?.url || 'initial load');
console.log('Search params:', searchParams.get('redirect'));
// Return boolean for allow/deny or string for redirect
if (!isCurrentUserAdmin()) {
return '/login?redirect=' + encodeURIComponent(to.url);
}
// Additional validation based on the user ID
if (!canAccessUser(userId)) {
return false; // Block navigation
}
return true; // Allow navigation
});
// Apply the guard to the route
const protectedRoute = pipe(
adminUserRoute,
guard(adminGuard)
);Nested Routes and Parallel Data Loading
When a nested route like /dashboard/users/123 is matched, Combi-Router builds a tree of match objects. If both dashboardRoute and userRoute have a loader, they are executed in parallel, and you can access data from any level of the hierarchy.
// dashboard-layout.ts
const dashboardRoute = pipe(
route(path('dashboard')),
loader(async () => ({ stats: await fetchDashboardStats() })),
layout(DashboardLayout) // Layout component with <Outlet />
);
// user-profile.ts
const userRoute = pipe(
extend(dashboardRoute, path('users'), param('id', z.number())),
loader(async ({ params }) => ({ user: await fetchUser(params.id) }))
);
// In your view for the user route, you can access both sets of data:
const dashboardData = router.currentMatch.data; // { stats: ... }
const userData = router.currentMatch.child.data; // { user: ... }Predictive Preloading
Improve perceived performance by loading a route's code and data before the user clicks a link. The router.peek() method is perfect for this.
// Preload on hover to make navigation feel instantaneous
myLink.addEventListener('mouseenter', () => {
router.peek(userRoute, { id: 123 });
});
// Navigate as usual on click
myLink.addEventListener('click', (e) => {
e.preventDefault();
router.navigate(userRoute, { id: 123 });
});View Transitions
Combi-Router automatically uses the browser's native View Transitions API for smooth, app-like page transitions. To enable it, simply add a CSS view-transition-name to elements that should animate between pages.
/* On a list page */
.product-thumbnail {
view-transition-name: product-image-123;
}
/* On a detail page */
.product-hero-image {
view-transition-name: product-image-123; /* Same name! */
}The router handles the rest. No JavaScript changes are needed.
🧩 Vanilla JS Utilities
Combi-Router is framework-agnostic at its core. To help you integrate it into a vanilla JavaScript project, we provide a set of utility functions. These helpers bridge the gap between the router's state and the DOM, making it easy to create navigable links, render nested views, and react to route changes.
Link & Navigation Helpers
createLink(router, route, params, options)
Creates a fully functional <a> element that navigates using the router. It automatically sets the href and intercepts click events to trigger client-side navigation. Each created link comes with a destroy function to clean up its event listeners.
import { createLink } from '@doeixd/combi-router/utils';
const { element, destroy } = createLink(
router,
userRoute,
{ id: 123 },
{ children: 'View Profile', className: 'btn' }
);
document.body.appendChild(element);
// Later, when the element is removed from the DOM:
// destroy();createActiveLink(router, route, params, options)
Builds on createLink to create an <a> element that automatically updates its CSS class when its route is active. This is perfect for navigation menus.
activeClassName: The CSS class to apply when the link is active.exact: Iftrue, the class is applied only on an exact route match. Iffalse(default), it's also applied for any active child routes.
import { createActiveLink } from '@doeixd/combi-router/utils';
const { element } = createActiveLink(router, dashboardRoute, {}, {
children: 'Dashboard',
className: 'nav-link',
activeClassName: 'font-bold' // Applied on /dashboard, /dashboard/users, etc.
});
document.querySelector('nav').appendChild(element);attachNavigator(element, router, route, params)
Makes any existing HTML element navigable. This is useful for turning buttons, divs, or other non-anchor elements into type-safe navigation triggers.
import { attachNavigator } from '@doeixd/combi-router/utils';
const myButton = document.getElementById('home-button');
const { destroy } = attachNavigator(myButton, router, homeRoute, {});Conditional Rendering
createOutlet(router, parentRoute, container, viewMap)
Provides a declarative "outlet" for nested routing, similar to <Outlet> in React Router or <router-view> in Vue. It listens for route changes and renders the correct child view into a specified container element.
parentRoute: The route of the component that contains the outlet.container: The DOM element where child views will be rendered.viewMap: An object mappingRoute.idto anElementFactoryfunction(match) => Node.
// In your dashboard layout component
import { createOutlet } from '@doeixd/combi-router/utils';
import { dashboardRoute, usersRoute, settingsRoute } from './routes';
import { UserListPage, SettingsPage } from './views';
const outletContainer = document.querySelector('#outlet');
createOutlet(router, dashboardRoute, outletContainer, {
[usersRoute.id]: (match) => new UserListPage(match.data), // Pass data to the view
[settingsRoute.id]: () => new SettingsPage(),
});createMatcher(router)
Creates a fluent, type-safe conditional tool that reacts to route changes. It's a powerful way to implement declarative logic that isn't tied directly to rendering.
import { createMatcher } from '@doeixd/combi-router/utils';
// Update the document title based on the active route
createMatcher(router)
.when(homeRoute, () => {
document.title = 'My App | Home';
})
.when(userRoute, (match) => {
document.title = `Profile for User ${match.params.id}`;
})
.otherwise(() => {
document.title = 'My App';
});State Management
createRouterStore(router)
Creates a minimal, framework-agnostic reactive store for the router's state (currentMatch, isNavigating, isFetching). This is useful for integrating with UI libraries or building your own reactive logic in vanilla JS.
import { createRouterStore } from '@doeixd/combi-router/utils';
const store = createRouterStore(router);
const unsubscribe = store.subscribe(() => {
const { isNavigating } = store.getSnapshot();
// Show a global loading indicator while navigating
document.body.style.cursor = isNavigating ? 'wait' : 'default';
});
// To clean up:
// unsubscribe();🎨 Web Components
For even simpler integration, Combi-Router provides ready-to-use Web Components that handle routing declaratively in your HTML:
<!DOCTYPE html>
<html>
<head>
<script type="module">
// Import standalone components (no setup required!)
import '@doeixd/combi-router/components-standalone';
</script>
</head>
<body>
<!-- Define your routes declaratively -->
<view-area match="/users/:id" view-id="user-detail"></view-area>
<view-area match="/about" view-id="about-page"></view-area>
<!-- Define your templates with automatic head management -->
<template is="view-template" view-id="user-detail">
<!-- Head automatically discovered and linked to view-area -->
<view-head
title="User Profile"
title-template="My App | %s"
description="View user profile and details"
og-title="User Profile"
og-description="Comprehensive user profile page"
og-type="profile">
</view-head>
<h1>User Details</h1>
<p>User ID: <span class="user-id"></span></p>
</template>
<template is="view-template" view-id="about-page">
<!-- Each template can have its own head configuration -->
<view-head
title="About Us"
description="Learn more about our company and mission"
keywords="about, company, mission, team"
canonical="https://myapp.com/about"
og-title="About Our Company"
og-description="Discover our story and values">
</view-head>
<h1>About</h1>
<p>This is the about page.</p>
</template>
<!-- Navigation works automatically -->
<nav>
<a href="/users/123">User 123</a>
<a href="/about">About</a>
</nav>
</body>
</html>Advanced Example with Nested Routes
<!-- Nested route structure -->
<view-area match="/dashboard" view-id="dashboard"></view-area>
<view-area match="/dashboard/users" view-id="users-list"></view-area>
<view-area match="/dashboard/users/:id" view-id="user-detail"></view-area>
<!-- Templates with automatic head discovery -->
<template is="view-template" view-id="dashboard">
<!-- Parent template head - automatically merges with child heads -->
<view-head
title="Dashboard"
title-template="Admin | %s"
description="Admin dashboard overview">
</view-head>
<h1>Dashboard</h1>
<nav>
<a href="/dashboard/users">Users</a>
<a href="/dashboard/analytics">Analytics</a>
</nav>
<main class="dashboard-content"></main>
</template>
<template is="view-template" view-id="users-list">
<!-- Child template head - merges with parent -->
<view-head
title="Users"
description="Manage users and permissions"
robots="noindex">
</view-head>
<h2>Users</h2>
<div class="users-grid"></div>
</template>
<!-- External template with dynamic head loading -->
<template is="view-template" view-id="user-detail" src="/views/user-detail.html"></template>
<!-- You can still use manual linking for external head configs -->
<view-head head-id="external-head" src="/head-configs/user-detail.js"></view-head>
<view-area match="/special/:id" view-id="special-view" head-id="external-head"></view-area>Key Benefits
- Zero JavaScript Configuration: Just import and use
- Declarative Routing: Define routes in HTML attributes
- Automatic Navigation: Links work out of the box
- SEO-Ready: Built-in head management with Open Graph and Twitter Cards
- Automatic Head Discovery: Place
view-headinside templates - no manual linking needed - Nested Head Management: Head tags merge hierarchically for complex layouts
- Dynamic Content: Load head configurations from external modules
- Flexible Linking: Choose automatic discovery or manual
head-idlinking - Progressive Enhancement: Works with or without JavaScript
- Dynamic Route Management: Add/remove routes programmatically when needed
⚙️ Configuration & API
🧰 Composable Layer Architecture
Combi-Router now features a revolutionary layer-based composition system using our custom makeLayered implementation, enabling true user extensibility while maintaining backwards compatibility.
Why Layers?
Traditional routers force you to choose between their built-in features or build everything from scratch. With layers, you can:
- Mix and match built-in features exactly as needed
- Create custom layers for your specific business logic
- Compose layers conditionally based on environment or feature flags
- Build orchestrated systems where layers can call each other's methods
- Maintain type safety with full TypeScript inference across all layers
Basic Layer Composition
import {
createLayeredRouter,
createCoreNavigationLayer,
withPerformance,
withScrollRestoration
} from '@doeixd/combi-router';
// Compose exactly the router you need
const router = createLayeredRouter(routes)
(createCoreNavigationLayer()) // Base navigation
(withPerformance({ prefetchOnHover: true })) // Performance optimizations
(withScrollRestoration({ strategy: 'smooth' })) // Scroll management
();
// All layer methods are now available
router.navigate('/user/123');
router.prefetchRoute('about');
router.saveScrollPosition();Custom Layer Creation
Create your own layers for analytics, authentication, or any business logic:
const withAnalytics = (config: { trackingId: string }) => (self: any) => {
// Register lifecycle hooks
if ('_registerLifecycleHook' in self) {
self._registerLifecycleHook('onNavigationStart', (context: any) => {
console.log(`[Analytics] Navigation started: ${context.to?.path}`);
});
self._registerLifecycleHook('onNavigationComplete', (match: any) => {
console.log(`[Analytics] Page view: ${match.path}`);
});
}
return {
trackEvent: