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>
  );
};

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>
  );
};

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.

  1. Global Redirects - Array-based redirects with multiple source patterns
  2. Route Map Redirects - Simple string-to-string redirects in the routes object
  3. Nested Route Matching - Hierarchical pattern matching with parameter inheritance
  4. Flat Route Matching - Traditional flat route handling as fallback
  5. 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.

  1. Pattern Parsing - Route patterns are split by / and analyzed for dynamic segments
  2. Segment Matching - Each URL segment is matched against its corresponding pattern segment
  3. Parameter Capture - :parameter segments extract their values into the params object
  4. Wildcard Handling - * segments capture all remaining path components
  5. Type Safety - TypeScript template literals provide compile-time parameter validation

The router coordinates navigation through a carefully orchestrated process.

  1. URL Change Detection - Listens for popstate events and programmatic navigation calls
  2. Route Resolution - Matches the new path against nested and flat route patterns
  3. Parameter Inheritance - Merges parameters from parent routes to child routes
  4. Hook Execution - Runs global and nested route hooks in proper cascading order
  5. Handler Invocation - Executes only the final matched route handler with all inherited parameters
  6. State Updates - Updates the reactive route signal to trigger component re-rendering
  7. 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