Reactive Client Routing
Reactive client-side routing that integrates seamlessly with reactivity system.
Router Setup
The router accepts a configuration object defining routes as pattern-to-handler mappings.
Route handlers execute immediately when their pattern matches, typically updating reactive state that drives UI changes.
import { router, navigate } from '@hellajs/router';
import { signal } from '@hellajs/core';
const currentView = signal('Loading...');
const renderView = (content) => { currentView(content); };
router({
routes: {
'/': () => renderView('Home'),
'/about': () => renderView('About'),
'/todos': () => renderView('TodoList'),
'/todos/:id': (params, query) => renderView(`TodoDetail: ${params.id}`)
},
notFound: () => renderView('404 Not Found')
});
Router Parameters
Dynamic Path Segments
Parameters are automatically extracted and passed as the first argument to route handlers.
import { router } from '@hellajs/router';
import { signal } from '@hellajs/core';
const currentView = signal('Loading...');
const selectedTodo = signal(null);
router({
routes: {
'/todos': () => currentView('TodoList'),
'/todos/:id': (params, query) => {
// params.id contains the captured segment
selectedTodo(params.id);
currentView(`TodoDetail: ${params.id}`);
return { todoId: params.id };
},
'/users/:userId/todos/:todoId': (params, query) => {
// Multiple parameters captured
currentView(`UserTodo: ${params.userId}/${params.todoId}`);
return { userId: params.userId, todoId: params.todoId };
}
}
});
Query Parameters
Query parameters are automatically parsed from the URL and passed as the second argument to route handlers.
router({
routes: {
'/search': (params, query) => {
// query contains parsed URL parameters
// /search?q=hello&category=work&sort=date
// query = { q: 'hello', category: 'work', sort: 'date' }
const searchResults = performSearch({
term: query.q,
category: query.category,
sort: query.sort
});
return searchResults;
},
'/todos': (params, query) => {
const filter = query.filter || 'all';
const page = parseInt(query.page || '1');
const todos = loadTodos(filter, page);
return { todos, filter, page };
}
}
});
Wildcard Routes
Wildcard routes capture remaining path segments using the *
syntax, useful for nested routing or file path handling.
router({
routes: {
'/files/*': (params, query) => {
// /files/documents/readme.md
// params['*'] = 'documents/readme.md'
const filePath = params['*'];
const fileContent = loadFileContent(filePath);
return { filePath, fileContent };
}
}
});
Router Configuration
Simple Function Handlers
Most routes use simple function handlers that execute when the path matches.
router({
routes: {
'/': () => showHomePage(),
'/about': () => showAboutPage(),
'/contact': () => showContactPage()
}
});
Route Objects with Hooks
Routes can be defined as objects with before
, after
, and handler
properties. Understanding hook execution order is critical for complex applications.
Hook Execution Order
Hooks execute in a sequential pattern for nested routes:
- Global before hooks
- All nested before hooks (parent to child order)
- Route handler execution
- All nested after hooks (child to parent reverse order)
- Global after hooks
import { router, navigate } from '@hellajs/router';
import { signal } from '@hellajs/core';
const currentView = signal('Loading...');
router({
routes: {
'/profile': {
before: () => {
// Check authentication before showing profile
if (!user()) {
navigate('/login');
return { redirected: true };
}
return { authenticated: true };
},
handler: (params, query) => {
currentView('UserProfile');
return { page: 'profile' };
},
after: () => {
// Log analytics after page loads
analytics.track('profile_viewed');
}
}
}
});
Nested Route Structures
Routes can be organized hierarchically using the children
property for better organization and automatic parameter inheritance.
import { router } from '@hellajs/router';
import { signal } from '@hellajs/core';
const currentView = signal('Loading...');
router({
routes: {
'/admin': {
handler: (params, query) => {
currentView('AdminDashboard');
return { section: 'dashboard' };
},
children: {
'/users': {
handler: (params, query) => {
currentView('UsersList');
return { users: loadUsersList() };
},
children: {
'/:id': (params, query) => {
currentView(`UserDetail: ${params.id}`);
return { userId: params.id, user: loadUser(params.id) };
}
}
},
'/settings': (params, query) => {
currentView('AdminSettings');
return { settings: loadSettings() };
}
}
}
}
});
Nested routes provide several benefits.
- Hierarchical Organization: Routes mirror your application’s structure
- Parameter Inheritance: Child routes automatically inherit parameters from parents
- Hook Cascading: Parent hooks execute before child hooks in proper order
- Fallback Handling: Parent routes can handle unmatched child paths
Parameter Inheritance in Nested Routes
Child routes automatically inherit all parameters from their parent routes, making it easy to build deeply nested structures.
import { router } from '@hellajs/router';
import { signal } from '@hellajs/core';
const currentView = signal('Loading...');
router({
routes: {
'/blog': {
children: {
'/:category': {
children: {
'/:postId': (params, query) => {
// Child route has access to parent parameters
const { category, postId } = params;
currentView(`Post: ${category}/${postId}`);
return {
category,
postId,
post: loadPost(postId)
};
}
}
}
}
}
}
});
// Visiting /blog/tech/my-post will pass:
// params = { category: 'tech', postId: 'my-post' }
Redirects and Hooks
The router supports two types of redirects and global lifecycle hooks:
String Redirects
Simple redirects can be defined using string values in the routes object.
router({
routes: {
'/home': '/', // Redirect /home to /
'/profile': '/user/me', // Redirect to current user profile
'/dashboard': '/todos' // Redirect dashboard to todos
}
});
Global Redirects
Multiple source paths can redirect to a single destination using the redirects array.
router({
routes: {
'/todos': () => showTodoList()
},
redirects: [
{ from: ['/tasks', '/items', '/list'], to: '/todos' }
]
});
Global Hooks
Global hooks run for every route change and execute at the outermost level of the cascade.
import { router, navigate } from '@hellajs/router';
import { signal } from '@hellajs/core';
const loading = signal(false);
const currentView = signal('Loading...');
router({
routes: {
'/admin': {
before: () => {
// Check admin permissions
if (!user()?.isAdmin) {
navigate('/login');
return { redirected: true };
}
return { adminCheck: true };
},
handler: (params, query) => {
currentView('AdminDashboard');
return { page: 'admin' };
},
after: () => {
// Log admin access
analytics.track('admin_accessed');
}
}
},
hooks: {
before: () => {
loading(true);
return { navigationStarted: true };
},
after: () => {
loading(false);
return { navigationCompleted: true };
}
}
});
// Execution order: Global before → Route before → Route handler → Route after → Global after
Hook Error Handling
Hook errors do not block navigation - they are logged and execution continues:
router({
routes: {
'/test': (params, query) => {
currentView('TestPage');
return { page: 'test' };
}
},
hooks: {
before: () => {
throw new Error('Navigation error');
// Navigation continues despite error
}
}
});
// Error is logged to console, but navigation completes successfully
Route Guards
Protect routes using before
hooks. Guards execute before handlers and can redirect or block access.
import { router, navigate } from '@hellajs/router';
import { signal } from '@hellajs/core';
const user = signal(null);
const currentView = signal('Loading...');
const requireAuth = () => {
if (!user()) {
navigate('/login');
return { authenticated: false, redirected: true };
}
return { authenticated: true, user: user() };
};
const requireAdmin = () => {
const currentUser = user();
if (!currentUser?.isAdmin) {
navigate('/dashboard');
return { adminCheck: false, redirected: true };
}
return { adminCheck: true };
};
router({
routes: {
'/login': (params, query) => {
currentView('Login');
return { page: 'login' };
},
'/admin': {
before: requireAuth,
children: {
'/users': {
before: requireAdmin,
handler: (params, query) => {
currentView('AdminUsers');
return { page: 'adminUsers', users: loadUsers() };
}
}
}
},
'/dashboard': {
before: requireAuth,
handler: (params, query) => {
currentView('Dashboard');
return { page: 'dashboard', user: user() };
}
}
}
});
Multiple Guard Pattern
// Multiple guards can be chained using route nesting
router({
routes: {
'/admin': {
before: requireAuth, // First guard
children: {
'/settings': {
before: requireAdmin, // Second guard
handler: () => {
currentView('AdminSettings');
return { settings: loadAdminSettings() };
}
}
}
}
}
});
Route Signal
Accessing Current Route State
The route signal provides reactive access to the current route information, including.
- Current Handler
- Route Parameters
- Query Strings
- Current Path
import { router, navigate, route } from '@hellajs/router';
import { signal } from '@hellajs/core';
const currentView = signal('Loading...');
router({
routes: {
'/': (params, query) => {
currentView('Home');
return { page: 'home' };
},
'/users/:id': (params, query) => {
currentView(`UserProfile: ${params.id}`);
return {
page: 'userProfile',
userId: params.id,
tab: query.tab || 'overview'
};
},
'/search': (params, query) => {
currentView(`SearchResults: ${query.q}`);
return {
page: 'search',
term: query.q,
results: performSearch(query.q)
};
}
}
});
// Access current route information reactively
const RouteDebugger = () => (
<div class="debug-panel">
<h3>Current Route:</h3>
<p>Path: {route().path}</p>
<p>Params: {JSON.stringify(route().params)}</p>
<p>Query: {JSON.stringify(route().query)}</p>
</div>
);
Route State Structure
The route
signal contains a RouteInfo
object with the following properties.
type RouteInfo = {
handler: RouteHandler | null; // Current route handler function
params: Record<string, string>; // URL parameters (:id, etc.)
query: Record<string, string>; // Query string parameters
path: string; // Current pathname + search
};
Reactive Route Components
Components can use the route
signal to reactively respond to navigation changes without being directly called by route handlers.
// Example: Navigation component using reactive route signal
const CurrentPage = () => {
const { path, params } = route();
return (
<div class="page-info">
<p>Current Path: {path}</p>
{params.id && <p>Item ID: {params.id}</p>}
</div>
);
};
// This component automatically updates when route changes
// No manual event listeners or state management needed
Navigation
Programmatic Navigation
The navigate function provides programmatic routing with support for parameters, query strings, and navigation options.
// Example: Component using programmatic navigation
const TodoList = () => {
const todos = signal([
{ id: 1, text: 'Learn HellaJS', done: false },
{ id: 2, text: 'Build todo app', done: false }
]);
const viewTodo = (id) => {
// Navigate with parameter substitution
navigate('/todos/:id', { id: String(id) });
};
const searchTodos = (term) => {
// Navigate with query parameters
navigate('/search', {}, { q: term });
};
return (
<div>
<ul>
{forEach(todos, todo => (
<li key={todo.id}>
<span>{todo.text}</span>
<button onClick={() => viewTodo(todo.id)}>View</button>
</li>
))}
</ul>
<button onClick={() => searchTodos('urgent')}>
Search
</button>
<button onClick={() => navigate('/todos/new')}>
Add Todo
</button>
</div>
);
};
Navigation Options
Call navigate
with the desired path, parameters, and query options.
// Replace current history entry (user cannot go back)
navigate('/login', {}, {}, { replace: true });
// Navigate with parameters and query (pattern substitution)
navigate('/users/:id/profile', { id: '123' }, { tab: 'settings' });
// Results in: /users/123/profile?tab=settings
// Navigate with both params and query
navigate('/users/:id', { id: '123' }, { tab: 'overview' });
// Results in: /users/123?tab=overview
Internal Mechanics
Reactive routing with minimal overhead while integrating seamlessly with HellaJS’s signal system.
Route Resolution Engine
The router implements a 5-phase resolution algorithm that processes routes in strict priority order. Understanding this algorithm is crucial for predicting route behavior in complex applications.
Phase 1: Global Redirects
// Processed first - highest priority
router({
redirects: [{ from: ['/old-path', '/legacy'], to: '/new-path' }],
routes: { '/new-path': () => handleNewPath() }
});
Phase 2: Route Map Redirects
// String values in routes object
router({
routes: {
'/home': '/', // Redirect /home to /
'/': () => handleHome()
}
});
Phase 3: Nested Route Matching
// Hierarchical routes with parameter inheritance
router({
routes: {
'/api': {
children: {
'/v1': {
children: {
'/users': () => handleUsers() // More specific wins
}
},
'/*': () => handleApiWildcard() // Less specific
}
}
}
});
Phase 4: Flat Route Matching
// Traditional flat routes as fallback
router({
routes: {
'/users/admin': () => handleAdminUser(), // Static segments prioritized
'/users/:id': (params, query) => handleUser(params.id) // Dynamic segments
}
});
Phase 5: Not Found Handler
// Final fallback for unmatched paths
router({
routes: { /* ... */ },
notFound: () => handle404()
});
Specificity Rules:
- Static segments beat dynamic parameters (
:id
) - Dynamic parameters beat wildcards (
*
) - Longer paths beat shorter paths
- Nested routes beat flat routes
- First match wins within same specificity level
Parameter Inheritance:
// /users/123/profile
router({
routes: {
'/users/:userId': {
children: {
'/profile': (params, query) => {
// params = { userId: '123' }
// Child inherits parent parameters automatically
return showUserProfile(params.userId);
}
}
}
}
});
Each phase uses early termination - once a match is found, processing stops immediately without checking subsequent phases.
Reactive State Management
The router maintains all internal state using signals from @hellajs/core
, creating a naturally reactive system.
Current Route Signal
↓
Route Handler Execution
↓
Component State Updates
↓
Automatic UI Re-rendering
The route
signal contains the complete routing context including handler, parameters, query string, and current path, enabling components to reactively respond to any routing changes.
Parameter Extraction Algorithm
Dynamic path matching uses a single-pass algorithm that efficiently extracts parameters.
- Pattern Parsing - Route patterns are split by
/
and analyzed for dynamic segments - Segment Matching - Each URL segment is matched against its corresponding pattern segment
- Parameter Capture -
:parameter
segments extract their values into the params object - Wildcard Handling -
*
segments capture all remaining path components - Type Safety - TypeScript template literals provide compile-time parameter validation
Navigation State Coordination
The router coordinates navigation through a 7-phase execution pipeline:
- URL Change Detection -
popstate
events andnavigate()
calls trigger route updates - Route Resolution - 5-phase algorithm (above) determines matching route
- Parameter Inheritance - Child routes inherit all parent parameters automatically
- Hook Execution - Before hooks execute in cascade order (global → parent → child)
- Handler Invocation - Only the final matched route handler executes
- State Updates - Reactive
route()
signal updates, triggering dependent computations - Cleanup Hooks - After hooks execute in reverse cascade order (child → parent → global)
Hook Execution Order:
// For route: /admin/users
router({
routes: {
'/admin': {
before: () => console.log('Admin before'),
after: () => console.log('Admin after'),
children: {
'/users': {
before: () => console.log('Users before'),
handler: () => console.log('Users handler'),
after: () => console.log('Users after')
}
}
}
},
hooks: {
before: () => console.log('Global before'),
after: () => console.log('Global after')
}
});
// Output: Global before → Admin before → Users before → Users handler → Users after → Admin after → Global after
History Management
The router provides intelligent history management using the HTML5 History API.
- History Mode - Uses
pushState
/replaceState
for clean URLs with server support requirements
The router maintains proper browser history behavior including back/forward navigation and bookmark support.
Browser Integration
The router provides seamless browser integration:
- History API - Uses
pushState
/replaceState
for clean URLs - Automatic Navigation - Handles back/forward button navigation
- URL Encoding - Proper parameter and query string encoding
- Server-Side Safe - All DOM operations are safely wrapped