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.
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
});
Creating Navigation Links
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
onBefore
onStart
onEnter
onExit
onSuccess
onError
// 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