Router state management
Learn how to access and manage router state, including current route information, parameters, and navigation tracking.
The Aurelia router provides several services for accessing and managing routing state. This section covers how to track current route information, access parameters, and monitor navigation state throughout your application.
Current Route Information with ICurrentRoute
ICurrentRouteThe ICurrentRoute service provides reactive access to the currently active route information. This is essential for components that need to respond to route changes or display route-specific information.
Basic Usage
import { ICurrentRoute } from '@aurelia/router';
import { resolve } from '@aurelia/kernel';
export class MyComponent {
private currentRoute = resolve(ICurrentRoute);
attached() {
// Access current route properties
console.log('Current path:', this.currentRoute.path);
console.log('Current URL:', this.currentRoute.url);
console.log('Page title:', this.currentRoute.title);
console.log('Query params:', this.currentRoute.query);
}
}Current Route Properties
path
string
The current route path (e.g., '/products/123')
url
string
The complete URL including query params and fragment
title
string
The current page title
query
URLSearchParams
Query parameters as URLSearchParams object
parameterInformation
ParameterInformation[]
Hierarchical parameter data
Working with Route Parameters
The parameterInformation property provides detailed parameter information for hierarchical routes:
export class ProductComponent {
private currentRoute = resolve(ICurrentRoute);
attached() {
// Access route parameters
const paramInfo = this.currentRoute.parameterInformation[0];
if (paramInfo?.params) {
const productId = paramInfo.params.id;
console.log('Product ID:', productId);
}
// Access nested route parameters
this.currentRoute.parameterInformation.forEach((info, index) => {
console.log(`Level ${index} params:`, info.params);
console.log(`Level ${index} viewport:`, info.viewport);
});
}
}Reactive Route State
The ICurrentRoute properties are observable and update automatically when navigation occurs:
import { watch } from '@aurelia/runtime';
export class NavigationTracker {
private currentRoute = resolve(ICurrentRoute);
@watch('currentRoute.path')
pathChanged(newPath: string, oldPath: string) {
console.log(`Route changed from ${oldPath} to ${newPath}`);
// Update analytics, breadcrumbs, etc.
}
@watch('currentRoute.query')
queryChanged(newQuery: URLSearchParams) {
// React to query parameter changes
const searchTerm = newQuery.get('q');
if (searchTerm) {
this.performSearch(searchTerm);
}
}
}Router Navigation State
Access the router's navigation state through the IRouter service:
import { IRouter } from '@aurelia/router';
import { resolve } from '@aurelia/kernel';
export class NavigationMonitor {
private router = resolve(IRouter);
checkNavigationState() {
// Check if router is currently navigating
if (this.router.isNavigating) {
console.log('Navigation in progress...');
}
// Access current route tree
const routeTree = this.router.routeTree;
console.log('Current route tree:', routeTree);
// Access current transition
const currentTransition = this.router.currentTr;
console.log('Transition ID:', currentTransition.id);
console.log('Transition trigger:', currentTransition.trigger);
}
}Managed History Entries (AuNavId and ManagedState)
AuNavId and ManagedState)Every time the router writes to the browser history it attaches an au-nav-id marker under the exported AuNavId constant. The router uses this managed state to detect backward versus forward navigation whenever a future popstate or hashchange event fires. Because the managed state flows through the router events API, you can read (or extend) it for diagnostics and per-entry state.
Inspect managed state during navigation
import { IRouterEvents, NavigationStartEvent } from '@aurelia/router';
import { resolve } from '@aurelia/kernel';
export class NavigationCorrelationService {
private readonly events = resolve(IRouterEvents);
public constructor() {
this.events.subscribe('au:router:navigation-start', (event: NavigationStartEvent) => {
if (event.managedState) {
console.log('History entry id:', event.managedState['au-nav-id']);
console.log('Restored filters:', event.managedState['filters']);
}
});
}
}Programmatic navigations (
router.load) start with an empty managed state.Browser-driven navigations (Back/Forward) reuse whatever was stored in
history.stateand surface it viaNavigationStartEvent.managedState.See Router events for additional patterns.
Persist extra metadata in history entries
You may attach your own keys to the active history entry as long as you keep the au-nav-id field intact. A common pattern is to listen for NavigationEndEvent, merge your metadata, and call window.history.replaceState:
import { IRouterEvents, NavigationEndEvent } from '@aurelia/router';
import { resolve } from '@aurelia/kernel';
export class HistoryMetadataService {
private readonly events = resolve(IRouterEvents);
public constructor() {
this.events.subscribe('au:router:navigation-end', (event: NavigationEndEvent) => {
const currentState = window.history.state ?? {};
const enhancedState = {
...currentState,
filters: this.captureFilters(),
updatedAt: Date.now(),
};
window.history.replaceState(enhancedState, document.title);
});
}
private captureFilters() {
// Replace with your own logic (for example grab data from a store or component)
return { tab: 'inbox' };
}
}When the user later taps the browser buttons, the router emits a NavigationStartEvent whose managedState contains the same metadata, allowing you to restore filter selections, scroll positions, or analytics context.
Navigation Context and Relative Navigation
Use route context for relative navigation and context-aware operations:
import { IRouteContext } from '@aurelia/router';
import { resolve } from '@aurelia/kernel';
export class ProductDetail {
private routeContext = resolve(IRouteContext);
private router = resolve(IRouter);
navigateToSibling() {
// Navigate relative to current context
return this.router.load('edit', { context: this.routeContext });
}
navigateToChild() {
// Navigate to child route
return this.router.load('reviews', { context: this.routeContext });
}
getRouteData() {
// Access route configuration data
const routeData = this.routeContext.data;
console.log('Route data:', routeData);
}
}Query Parameters and Fragments
Reading Query Parameters
export class SearchComponent {
private currentRoute = resolve(ICurrentRoute);
attached() {
// Read query parameters
const query = this.currentRoute.query;
const searchTerm = query.get('q');
const page = parseInt(query.get('page') || '1');
const filters = query.getAll('filter');
console.log('Search term:', searchTerm);
console.log('Page:', page);
console.log('Filters:', filters);
}
// Convert query params to object
getQueryObject(): Record<string, string | string[]> {
const query = this.currentRoute.query;
const result: Record<string, string | string[]> = {};
for (const [key, value] of query.entries()) {
if (key in result) {
// Multiple values for same key
const existing = result[key];
result[key] = Array.isArray(existing) ? [...existing, value] : [existing as string, value];
} else {
result[key] = value;
}
}
return result;
}
}Setting Query Parameters
export class FilterComponent {
private router = resolve(IRouter);
private currentRoute = resolve(ICurrentRoute);
applyFilters(filters: string[]) {
// Navigate with new query parameters
return this.router.load(this.currentRoute.path, {
queryParams: {
filter: filters,
page: '1'
}
});
}
updateSearchTerm(term: string) {
// Update single query parameter
const currentQuery = this.currentRoute.query;
const newParams = new URLSearchParams(currentQuery);
newParams.set('q', term);
return this.router.load(this.currentRoute.path, {
queryParams: Object.fromEntries(newParams)
});
}
}Route State in Templates
Access current route information directly in templates:
<!-- Display current route information -->
<div class="route-info">
<h1>${currentRoute.title}</h1>
<p>Path: ${currentRoute.path}</p>
<p>Query: ${currentRoute.query.toString()}</p>
</div>
<!-- Conditional rendering based on route -->
<div if.bind="currentRoute.path.startsWith('/admin')">
<admin-sidebar></admin-sidebar>
</div>
<!-- Access route parameters -->
<div repeat.for="paramInfo of currentRoute.parameterInformation">
<h3>Level ${$index} Parameters:</h3>
<ul>
<li repeat.for="[key, value] of paramInfo.params | entries">
${key}: ${value}
</li>
</ul>
</div>Best Practices
1. Use Reactive Patterns
Always prefer watching route changes over manually checking route state:
// ✅ Good - Reactive approach
@watch('currentRoute.parameterInformation')
paramsChanged(newParams: ParameterInformation[]) {
this.loadData();
}
// ❌ Avoid - Manual polling
setInterval(() => {
const params = this.currentRoute.parameterInformation;
// Check for changes manually
}, 1000);2. Handle Missing Parameters
Always handle cases where route parameters might not exist:
getProductId(): string | null {
const paramInfo = this.currentRoute.parameterInformation[0];
return paramInfo?.params?.id ?? null;
}3. Cleanup Subscriptions
When manually subscribing to route changes, ensure proper cleanup:
export class ComponentWithSubscription {
private routeSubscription?: IDisposable;
attached() {
this.routeSubscription = this.routerEvents.subscribe(
'au:router:navigation-end',
() => this.handleNavigation()
);
}
detached() {
this.routeSubscription?.dispose();
}
}Preserve Scroll Positions with IStateManager
IStateManagerThe router ships an IStateManager service that captures scroll offsets for every descendant element inside a routed component. Pair it with lifecycle hooks to remember where the user left off when they revisit the same view.
Component-level usage
import { IRouteViewModel, IStateManager } from '@aurelia/router';
import { ICustomElementController } from '@aurelia/runtime-html';
import { resolve } from '@aurelia/kernel';
export class ArticleList implements IRouteViewModel {
private readonly controller = resolve(ICustomElementController);
private readonly stateManager = resolve(IStateManager);
canUnload() {
this.stateManager.saveState(this.controller);
return true;
}
loading() {
this.stateManager.restoreState(this.controller);
}
}saveStatewalks the DOM tree and records scroll positions for elements that have been scrolled.restoreStatereplays those offsets the next time the component loads.The data is stored in a
WeakMap, so it is automatically released when the component is destroyed.
Share scroll persistence across multiple routes
Router hooks run outside the component's own dependency injection scope, so they cannot access an ICustomElementController automatically. Instead, place the scroll logic in a shared base class (or mixin) that each routed component can extend. That way every component still resolves its own controller, but the implementation lives in one place.
import { IRouteViewModel, IStateManager } from '@aurelia/router';
import { ICustomElementController } from '@aurelia/runtime-html';
import { resolve } from '@aurelia/kernel';
export abstract class ScrollAwareRoute implements IRouteViewModel {
protected readonly controller = resolve(ICustomElementController);
protected readonly stateManager = resolve(IStateManager);
canUnload() {
this.stateManager.saveState(this.controller);
return true;
}
loading() {
this.stateManager.restoreState(this.controller);
}
}
export class ArticleList extends ScrollAwareRoute {}
export class ArticleDetail extends ScrollAwareRoute {}Any routed view-model that extends ScrollAwareRoute automatically persists scroll positions without hand-copying the logic.
This enhanced router state management documentation provides developers with comprehensive guidance on accessing and managing routing state, filling a critical gap in the current documentation.
Last updated
Was this helpful?