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:

  1. Global before hooks
  2. All nested before hooks (parent to child order)
  3. Route handler execution
  4. All nested after hooks (child to parent reverse order)
  5. 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

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

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.

  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 7-phase execution pipeline:

  1. URL Change Detection - popstate events and navigate() calls trigger route updates
  2. Route Resolution - 5-phase algorithm (above) determines matching route
  3. Parameter Inheritance - Child routes inherit all parent parameters automatically
  4. Hook Execution - Before hooks execute in cascade order (global → parent → child)
  5. Handler Invocation - Only the final matched route handler executes
  6. State Updates - Reactive route() signal updates, triggering dependent computations
  7. 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