Skip to main content
This guide will walk you through creating a complete working router using UI-Router Core. You’ll learn how to create a router instance, register states, handle navigation, and work with parameters.

Prerequisites

Ensure you have UI-Router Core installed in your project. If not, see the Installation guide.

Step 1: Create and Configure the Router

First, create a UIRouter instance and add the location plugin:
import { UIRouter } from '@uirouter/core';
import { pushStateLocationPlugin } from '@uirouter/core';

// Create the router instance
const router = new UIRouter();

// Add HTML5 pushState support
router.plugin(pushStateLocationPlugin);
This example uses pushStateLocationPlugin for clean URLs. You can use hashLocationPlugin instead if you prefer hash-based routing or cannot configure server-side URL rewriting.

Step 2: Register States

Define your application states using the StateRegistry:
// Register the home state
router.stateRegistry.register({
  name: 'home',
  url: '/',
  // In a real app, you'd render a component here
  onEnter: () => {
    document.getElementById('app').innerHTML = '<h1>Home Page</h1>';
  }
});

// Register an about state
router.stateRegistry.register({
  name: 'about',
  url: '/about',
  onEnter: () => {
    document.getElementById('app').innerHTML = '<h1>About Page</h1>';
  }
});

// Register a user detail state with parameters
router.stateRegistry.register({
  name: 'user',
  url: '/user/:userId',
  onEnter: (transition) => {
    const userId = transition.params().userId;
    document.getElementById('app').innerHTML = 
      `<h1>User Profile</h1><p>User ID: ${userId}</p>`;
  }
});

Understanding State Configuration

Each state declaration includes:
  • name: Unique identifier for the state (required)
  • url: URL pattern for the state
  • onEnter: Hook called when entering the state
  • onExit: Hook called when leaving the state
In framework-specific implementations (Angular, React, etc.), you’d specify a component instead of using onEnter hooks.

Step 3: Create Navigation

Use the StateService to navigate between states programmatically:
// Navigate to a state by name
router.stateService.go('home');

// Navigate with parameters
router.stateService.go('user', { userId: 123 });

// Navigate with options
router.stateService.go('about', {}, { 
  reload: true,    // Force reload even if already on the state
  notify: true     // Trigger transition hooks
});
For HTML navigation, you can create helper functions:
function createNavLink(stateName: string, params = {}, text: string) {
  const link = document.createElement('a');
  link.textContent = text;
  link.href = router.stateService.href(stateName, params);
  
  link.addEventListener('click', (e) => {
    e.preventDefault();
    router.stateService.go(stateName, params);
  });
  
  return link;
}

// Usage
const homeLink = createNavLink('home', {}, 'Home');
const aboutLink = createNavLink('about', {}, 'About');
const userLink = createNavLink('user', { userId: 42 }, 'User Profile');

Step 4: Start the Router

Enable URL listening and synchronize with the current URL:
// Start listening to URL changes
router.urlService.listen();

// Sync with the current browser URL
router.urlService.sync();
Always call listen() before sync(). The listen() method starts monitoring URL changes, while sync() performs the initial routing based on the current URL.

Step 5: Working with Nested States

Create parent-child state relationships:
// Parent state
router.stateRegistry.register({
  name: 'admin',
  url: '/admin',
  abstract: true,  // Cannot be activated directly
  onEnter: () => {
    console.log('Entering admin area');
  }
});

// Child states inherit URL and properties from parent
router.stateRegistry.register({
  name: 'admin.dashboard',
  url: '/dashboard',
  onEnter: () => {
    document.getElementById('app').innerHTML = '<h1>Admin Dashboard</h1>';
  }
  // Full URL will be: /admin/dashboard
});

router.stateRegistry.register({
  name: 'admin.users',
  url: '/users',
  onEnter: () => {
    document.getElementById('app').innerHTML = '<h1>User Management</h1>';
  }
  // Full URL will be: /admin/users  
});

Benefits of Nested States

Child state URLs are automatically composed with parent URLs. A state admin.users with url /users under parent admin with url /admin creates the full URL /admin/users.
Child states inherit resolve data, parameters, and other properties from parent states.
Parent state hooks run before child state hooks, allowing for setup/teardown logic.

Step 6: Using Resolve for Async Data

Fetch data before entering a state:
// Mock API service
const UserService = {
  getUser: (id: number) => {
    return new Promise((resolve) => {
      setTimeout(() => {
        resolve({ id, name: `User ${id}`, email: `user${id}@example.com` });
      }, 500);
    });
  }
};

router.stateRegistry.register({
  name: 'userDetail',
  url: '/user/:userId',
  resolve: [
    {
      token: 'user',
      deps: [Transition],
      resolveFn: (transition) => {
        const userId = transition.params().userId;
        return UserService.getUser(userId);
      }
    }
  ],
  onEnter: (transition) => {
    // Resolved data is available in the transition injector
    const user = transition.injector().get('user');
    document.getElementById('app').innerHTML = `
      <h1>${user.name}</h1>
      <p>Email: ${user.email}</p>
      <p>ID: ${user.id}</p>
    `;
  }
});
The state won’t activate until all resolves complete successfully. If a resolve rejects, the transition is aborted.

Step 7: Adding Transition Hooks

Attach behavior to state transitions:
import { Transition } from '@uirouter/core';

// Log all state transitions
router.transitionService.onStart({}, (transition: Transition) => {
  console.log(`Navigating from ${transition.from().name} to ${transition.to().name}`);
});

// Require authentication for admin states
router.transitionService.onBefore(
  { to: 'admin.**' },  // Match admin and all descendants
  (transition: Transition) => {
    const isAuthenticated = checkAuth(); // Your auth check function
    
    if (!isAuthenticated) {
      // Redirect to login
      return router.stateService.target('login', {
        returnTo: transition.to().name
      });
    }
  }
);

// Track successful transitions
router.transitionService.onSuccess({}, (transition: Transition) => {
  console.log('Transition completed:', transition.to().name);
  // Update analytics, etc.
});

// Handle transition errors
router.transitionService.onError({}, (transition: Transition) => {
  console.error('Transition failed:', transition.error());
});

Available Hook Types

// Runs before transition starts
// Can cancel or redirect the transition
router.transitionService.onBefore(criteria, callback);

Step 8: Working with Parameters

Handle different types of parameters:
// URL parameters (in the path)
router.stateRegistry.register({
  name: 'product',
  url: '/product/:productId',
  // productId is required and in the URL
});

// Query parameters
router.stateRegistry.register({
  name: 'search',
  url: '/search?query&page',
  // query and page are optional query parameters
});

// Typed parameters
router.stateRegistry.register({
  name: 'article',
  url: '/article/:articleId',
  params: {
    articleId: { 
      type: 'int',  // Built-in type coercion
      value: null   // Default value
    },
    showComments: {
      type: 'bool',
      value: true,
      squash: true  // Remove from URL if value is default
    }
  }
});

// Navigate with parameters
router.stateService.go('search', {
  query: 'ui-router',
  page: 2
});

// Access current parameters
const currentParams = router.stateService.params;
console.log(currentParams.query, currentParams.page);

Parameter Types

Plain string values, no encoding/decoding
Converts to/from integers. Non-integer values are rejected.
Converts to/from booleans. Accepts true, false, 0, 1, "true", "false".
Converts to/from Date objects. Encodes as ISO 8601 string.
Encodes/decodes as JSON strings. Useful for complex objects.

Complete Example

Here’s a complete working example combining all the concepts:
import { UIRouter, Transition } from '@uirouter/core';
import { pushStateLocationPlugin } from '@uirouter/core';

// Create and configure router
const router = new UIRouter();
router.plugin(pushStateLocationPlugin);

// Mock data service
const DataService = {
  getUsers: () => Promise.resolve([
    { id: 1, name: 'Alice' },
    { id: 2, name: 'Bob' }
  ]),
  getUser: (id: number) => Promise.resolve(
    { id, name: `User ${id}`, email: `user${id}@example.com` }
  )
};

// Register states
router.stateRegistry.register({
  name: 'home',
  url: '/',
  onEnter: () => {
    render('<h1>Home</h1><p>Welcome to UI-Router Core</p>');
  }
});

router.stateRegistry.register({
  name: 'users',
  url: '/users',
  resolve: [
    {
      token: 'users',
      resolveFn: () => DataService.getUsers()
    }
  ],
  onEnter: (transition: Transition) => {
    const users = transition.injector().get('users');
    const html = `
      <h1>Users</h1>
      <ul>
        ${users.map(u => `<li><a href="${router.stateService.href('user', {userId: u.id})}">${u.name}</a></li>`).join('')}
      </ul>
    `;
    render(html);
  }
});

router.stateRegistry.register({
  name: 'user',
  url: '/user/:userId',
  params: {
    userId: { type: 'int' }
  },
  resolve: [
    {
      token: 'user',
      deps: [Transition],
      resolveFn: (trans: Transition) => DataService.getUser(trans.params().userId)
    }
  ],
  onEnter: (transition: Transition) => {
    const user = transition.injector().get('user');
    const html = `
      <h1>${user.name}</h1>
      <p>Email: ${user.email}</p>
      <p><a href="${router.stateService.href('users')}">Back to Users</a></p>
    `;
    render(html);
  }
});

// Add global transition logging
router.transitionService.onStart({}, (transition: Transition) => {
  console.log(`Transition: ${transition.from().name} -> ${transition.to().name}`);
});

// Utility render function
function render(html: string) {
  const app = document.getElementById('app');
  if (app) app.innerHTML = html;
}

// Start the router
router.urlService.listen();
router.urlService.sync();

// Export for global access
export { router };

Debugging and Tracing

Enable detailed logging to debug your router:
// Enable all tracing
router.trace.enable('TRANSITION');

// Available trace categories
router.trace.enable('RESOLVE');      // Resolve data fetching
router.trace.enable('VIEWCONFIG');   // View configuration
router.trace.enable('UIVIEW');       // UI-View rendering
Use tracing during development to understand transition flow, but disable it in production for better performance.

Next Steps

Now that you have a working router, explore these topics:

Core Concepts

Understand states, transitions, and the state tree in depth

State Configuration

Learn all state configuration options and patterns

Transition Hooks

Master the transition lifecycle and hook types

URL Routing

Advanced URL patterns and parameter handling

Build docs developers (and LLMs) love