Reactive Client Routing
Reactive client-side routing that integrates seamlessly with reactivity system. Routes are defined as simple functions that execute when paths match, enabling reactive navigation patterns without complex lifecycle management.
Basic Setup
The router accepts a configuration object defining routes as pattern-to-handler mappings.
Route handlers execute immediately when their pattern matches, typically updating a reactive signal that drives UI changes.
import { mount } from '@hellajs/dom';
import { router, navigate } from '@hellajs/router';
mount(<>Loading...</>);
router({
routes: {
'/': () => mount(<Home />),
'/about': () => mount(<About />),
'/todos': () => mount(<TodoList />),
'/todos/:id': (params) => mount(<TodoDetail id={params.id} />)
},
notFound: () => mount(<NotFound />)
});
Router Parameters
Dynamic Path Segments
Parameters are automatically extracted and passed as the first argument to route handlers.
import { mount } from '@hellajs/dom';
import { router } from '@hellajs/router';
import { signal } from '@hellajs/core';
mount(<>Loading...</>);
const selectedTodo = signal(null);
router({
routes: {
'/todos': () => mount(<TodoList />),
'/todos/:id': (params) => {
// params.id contains the captured segment
selectedTodo(params.id);
mount(<TodoDetail todoId={params.id} />);
},
'/users/:userId/todos/:todoId': (params) => {
// Multiple parameters captured
mount(<UserTodo 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' }
mount(<SearchResults
term={query.q}
category={query.category}
sort={query.sort}
/>);
},
'/todos': (params, query) => {
const filter = query.filter || 'all';
const page = parseInt(query.page || '1');
mount(<TodoList filter={filter} page={page} />);
}
}
});
Wildcard Routes
Wildcard routes capture remaining path segments using the *
syntax, useful for nested routing or file path handling.
router({
routes: {
'/files/*': (params) => {
// /files/documents/readme.md
// params['*'] = 'documents/readme.md'
const filePath = params['*'];
mount(<FileViewer path={filePath} />);
}
}
});
Router Configuration
Simple Function Handlers
Most routes use simple function handlers that execute when the path matches.
router({
routes: {
'/': () => mount(<Home />),
'/about': () => mount(<About />),
'/contact': () => mount(<Contact />)
}
});
Route Objects with Hooks
Routes can be defined as objects with before
, after
, and handler
properties for more complex behavior.
import { mount } from '@hellajs/dom';
import { router } from '@hellajs/router';
import { signal } from '@hellajs/core';
mount(<>Loading...</>);
const count = signal(0);
router({
routes: {
'/counter': {
before: () => {
console.log('Navigating to counter');
count(0); // Reset counter
},
handler: () => mount(<CounterView count={count} />),
after: () => {
console.log('Counter page loaded');
}
}
}
});
Nested Route Structures
Routes can be organized hierarchically using the children
property for better organization and automatic parameter inheritance.
import { mount } from '@hellajs/dom';
import { router, navigate } from '@hellajs/router';
mount(<>Loading...</>);
router({
routes: {
'/admin': {
handler: () => mount(<AdminDashboard />),
children: {
'/users': {
handler: () => mount(<UsersList />),
children: {
'/:id': (params) => mount(<UserDetail userId={params.id} />)
}
},
'/settings': {
handler: () => mount(<AdminSettings />),
children: {
'/general': () => mount(<GeneralSettings />),
'/security': () => mount(<SecuritySettings />)
}
}
}
}
}
});
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 { mount } from '@hellajs/dom';
import { router } from '@hellajs/router';
mount(<>Loading...</>);
router({
routes: {
'/blog': {
children: {
'/:category': {
children: {
'/:postId': {
children: {
'/comments': {
children: {
'/:commentId': (params) => {
// Child route has access to all parent parameters
const { category, postId, commentId } = params;
mount(
<CommentDetail
category={category}
postId={postId}
commentId={commentId}
/>
);
}
}
}
}
}
}
}
}
}
}
});
// Visiting /blog/tech/my-post/comments/123 will pass.
// params = { category: 'tech', postId: 'my-post', commentId: '123' }
String Redirects
Simple redirects can be defined using string values.
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': () => mount(<TodoList />)
},
redirects: [
{ from: ['/tasks', '/items', '/list'], to: '/todos' }
]
});
Global Hooks
Define hooks that run for all route changes.
import { mount } from '@hellajs/dom';
import { router } from '@hellajs/router';
import { signal } from '@hellajs/core';
mount(<>Loading...</>);
const loading = signal(false);
router({
routes: {
'/': () => mount(<Home />),
'/todos': () => mount(<TodoList />)
},
hooks: {
before: () => {
loading(true);
console.log('Navigation starting');
},
after: () => {
loading(false);
console.log('Navigation complete');
}
}
});
Route Guards
Protect routes using before
hooks.
import { mount } from '@hellajs/dom';
import { router, navigate } from '@hellajs/router';
import { signal } from '@hellajs/core';
mount(<>Loading...</>);
const user = signal(null);
const requireAuth = () => {
if (!user()) {
navigate('/login');
return; // Redirect to login
}
// User is authenticated, continue with route
};
const setUser = (userData) => user(userData);
router({
routes: {
'/login': () => mount(<Login onLogin={setUser} />),
'/dashboard': {
before: requireAuth,
handler: () => mount(<Dashboard user={user} />)
},
'/profile': {
before: requireAuth,
handler: () => mount(<Profile user={user} />)
}
},
notFound: () => mount(<NotFound />)
});
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 { mount } from '@hellajs/dom';
import { router, navigate, route } from '@hellajs/router';
mount(<>Loading...</>);
router({
routes: {
'/': () => mount(<Home />),
'/users/:id': (params, query) => {
mount(<UserProfile userId={params.id} tab={query.tab} />);
},
'/search': (params, query) => {
mount(<SearchResults term={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.
const Breadcrumbs = () => {
const generateBreadcrumbs = () => {
const { path, params } = route();
const segments = path.split('/').filter(Boolean);
return segments.map((segment, index) => {
const isParam = segment.startsWith(':');
const label = isParam ? params[segment.slice(1)] : segment;
const href = '/' + segments.slice(0, index + 1).join('/');
return { label, href, isLast: index === segments.length - 1 };
});
};
return (
<nav class="breadcrumbs">
<a href="/">Home</a>
{forEach(generateBreadcrumbs, crumb => (
<span key={crumb.href}>
<span class="separator"> / </span>
{crumb.isLast ? (
<span class="current">{crumb.label}</span>
) : (
<a href={crumb.href}>{crumb.label}</a>
)}
</span>
))}
</nav>
);
};
Navigation
Programmatic Navigation
The navigate function provides programmatic routing with support for parameters, query strings, and navigation options.
const TodoList = () => {
const todos = signal([
{ id: 1, text: 'Learn HellaJS', done: false },
{ id: 2, text: 'Build todo app', done: false }
]);
const viewTodo = (id) => {
navigate(`/todos/${id}`);
};
const searchTodos = (term) => {
navigate('/search', {}, { q: term, type: 'todos' });
};
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 Urgent
</button>
</div>
);
};
Navigation Options
Call navigate
with the desired path, parameters, and query options.
// Replace current history entry
navigate('/login', {}, {}, { replace: true });
// Navigate with parameters and query (pattern substitution)
navigate('/users/:id/profile', { id: '123' }, { tab: 'settings' });
Internal Mechanics
Reactive routing with minimal overhead while integrating seamlessly with HellaJS’s signal system.
Route Resolution Engine
The router uses a sophisticated multi-phase resolution system that processes routes in order of precedence.
- Global Redirects - Array-based redirects with multiple source patterns
- Route Map Redirects - Simple string-to-string redirects in the routes object
- Nested Route Matching - Hierarchical pattern matching with parameter inheritance
- Flat Route Matching - Traditional flat route handling as fallback
- Not Found Handler - Fallback for unmatched paths
The nested route matcher prioritizes more specific routes over wildcards and uses intelligent route sorting to ensure correct precedence. Parameter inheritance is handled automatically during the matching process, with child parameters potentially overriding parent parameters of the same name.
Each phase uses early termination to minimize processing overhead, with the first matching route immediately executing without checking subsequent patterns.
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 carefully orchestrated process.
- URL Change Detection - Listens for
popstate
events and programmatic navigation calls - Route Resolution - Matches the new path against nested and flat route patterns
- Parameter Inheritance - Merges parameters from parent routes to child routes
- Hook Execution - Runs global and nested route hooks in proper cascading order
- Handler Invocation - Executes only the final matched route handler with all inherited parameters
- State Updates - Updates the reactive route signal to trigger component re-rendering
- Cleanup Hooks - Runs nested
after
hooks in reverse order for proper cleanup
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.
Memory Efficiency
The router optimizes memory usage through several mechanisms.
- Event Listener Management - Single global listeners handle all navigation events
- Parameter Object Reuse - Route parameters are efficiently parsed and cached
- Signal Integration - Leverages core signal system for automatic cleanup and subscription management
- Lazy Route Processing - Routes are only processed when navigation occurs, not during definition
Browser Compatibility
The router includes comprehensive browser compatibility features.
- Server-Side Safety - All DOM interactions are wrapped in
typeof window
checks - History API Fallback - Graceful degradation when history API is unavailable
- URL Encoding - Proper encoding/decoding of parameters and query strings for international character support
- Event Normalization - Consistent event handling across different browser implementations