The router module provides SPA-style client-side routing built on the History API. It integrates seamlessly with bQuery's reactive system.
Internally, the router is now split into focused submodules (matching, navigation, state, links, utilities). The public API remains unchanged.
import { createRouter, navigate, currentRoute } from '@bquery/bquery/router';
import { effect } from '@bquery/bquery/reactive';Basic Setup
const router = createRouter({
routes: [
{ path: '/', component: () => import('./pages/Home') },
{ path: '/about', component: () => import('./pages/About') },
{ path: '/user/:id', component: () => import('./pages/User') },
{ path: '*', component: () => import('./pages/NotFound') },
],
});
// React to route changes
effect(() => {
const route = currentRoute.value;
console.log('Current path:', route.path);
console.log('Params:', route.params);
});Navigation
import { navigate, back, forward } from '@bquery/bquery/router';
// Push to history
await navigate('/dashboard');
// Replace current entry
await navigate('/login', { replace: true });
// Browser history
back();
forward();Route Params
Dynamic segments are defined with :paramName:
const router = createRouter({
routes: [
{ path: '/user/:id', component: () => import('./User') },
{ path: '/post/:slug/comment/:commentId', component: () => import('./Comment') },
],
});
// Navigating to /user/42
console.log(currentRoute.value.params); // { id: '42' }Query Params
Query strings are automatically parsed:
// URL: /search?q=hello&page=2
console.log(currentRoute.value.query); // { q: 'hello', page: '2' }
// Repeated keys become arrays
// URL: /search?tag=js&tag=ts
console.log(currentRoute.value.query); // { tag: ['js', 'ts'] }Navigation Guards
beforeEach
Run logic before every navigation. Return false to cancel:
router.beforeEach((to, from) => {
if (to.path === '/admin' && !isAuthenticated()) {
navigate('/login');
return false;
}
});afterEach
Run logic after successful navigation:
router.afterEach((to, from) => {
analytics.track('pageview', { path: to.path });
});Removing Guards
Both methods return a cleanup function:
const removeGuard = router.beforeEach((to, from) => {
// ...
});
// Later
removeGuard();Named Routes
Define route names for easier programmatic navigation:
const router = createRouter({
routes: [
{ path: '/', name: 'home', component: () => import('./Home') },
{ path: '/user/:id', name: 'user', component: () => import('./User') },
],
});
// Resolve by name
import { resolve } from '@bquery/bquery/router';
const path = resolve('user', { id: '42' });
// Returns '/user/42'Active Link Detection
import { isActive, isActiveSignal } from '@bquery/bquery/router';
// Immediate check
if (isActive('/dashboard')) {
navItem.classList.add('active');
}
// Reactive check
const dashboardActive = isActiveSignal('/dashboard');
effect(() => {
navItem.classList.toggle('active', dashboardActive.value);
});
// Exact matching
isActive('/dashboard', true); // Only matches exactly '/dashboard'Link Helpers
Manual Link Handler
import { link } from '@bquery/bquery/router';
import { $ } from '@bquery/bquery/core';
$('#nav-home').on('click', link('/'));
$('#nav-about').on('click', link('/about'));Automatic Link Interception
Intercept all internal links in a container:
import { interceptLinks } from '@bquery/bquery/router';
// Intercept all links in document
const cleanup = interceptLinks(document.body);
// Links with target, download, or external URLs are ignoredHash Mode
Use hash-based routing for static hosting:
const router = createRouter({
routes: [...],
hash: true, // URLs like /#/about
});Base Path
Prefix all routes with a base path:
const router = createRouter({
routes: [...],
base: '/app', // Routes are relative to /app
});Lazy Loading
Components can be loaded lazily:
const router = createRouter({
routes: [
{
path: '/dashboard',
component: async () => {
const module = await import('./pages/Dashboard');
return module.default;
},
},
],
});Nested Routes
Define child routes for complex layouts:
const router = createRouter({
routes: [
{
path: '/dashboard',
component: () => import('./Dashboard'),
children: [
{ path: '/settings', component: () => import('./Settings') },
{ path: '/profile', component: () => import('./Profile') },
],
},
],
});
// Results in:
// /dashboard -> Dashboard
// /dashboard/settings -> Settings
// /dashboard/profile -> ProfileCleanup
Destroy the router when no longer needed:
router.destroy();Type Reference
type Route = {
path: string;
params: Record<string, string>;
query: Record<string, string | string[]>;
matched: RouteDefinition | null;
hash: string;
};
type RouteDefinition = {
path: string;
component: () => unknown | Promise<unknown>;
name?: string;
meta?: Record<string, unknown>;
children?: RouteDefinition[];
};
type RouterOptions = {
routes: RouteDefinition[];
base?: string;
hash?: boolean;
};
type NavigationGuard = (to: Route, from: Route) => boolean | void | Promise<boolean | void>;