Skip to main content

Transition Hooks

Transition hooks allow you to tap into the state transition lifecycle. Use them for authentication, authorization, logging, validation, and data preparation.

Hook Types

UI-Router provides several hook types that fire at different points in the transition lifecycle:
transitionService.onCreate()   // When transition is created
transitionService.onBefore()   // Before transition starts
transitionService.onStart()    // Transition starts
transitionService.onExit()     // State is being exited
transitionService.onRetain()   // State is being retained
transitionService.onEnter()    // State is being entered
transitionService.onFinish()   // Transition finishing
transitionService.onSuccess()  // Transition succeeded
transitionService.onError()    // Transition failed

Hook Lifecycle

Hooks execute in this order during a transition:
1
onCreate
2
Transition object is created (synchronous)
3
onBefore
4
Before transition starts - can cancel or redirect
5
onStart
6
Transition begins execution
7
onExit
8
States being exited (reverse order, child to parent)
9
onRetain
10
States being retained
11
onEnter
12
States being entered (parent to child order)
13
onFinish
14
Transition is finishing
15
onSuccess or onError
16
Transition completed successfully or with error

Registering Hooks

Basic Hook Registration

import { UIRouter } from '@uirouter/core';

const router = new UIRouter();
const transitionService = router.transitionService;

// Register a hook
transitionService.onStart({}, (transition) => {
  console.log('Transition started to:', transition.to().name);
});

Hook Criteria

Target specific transitions using criteria:
// Match specific state
transitionService.onEnter({ entering: 'dashboard' }, (transition) => {
  console.log('Entering dashboard');
});

// Match multiple states
transitionService.onEnter({ entering: ['users', 'admin'] }, (transition) => {
  console.log('Entering users or admin');
});

// Match state pattern (glob)
transitionService.onEnter({ entering: 'admin.**' }, (transition) => {
  console.log('Entering any admin state');
});

// Match by state property
transitionService.onBefore({ to: (state) => state.data?.requiresAuth }, (transition) => {
  // Check authentication
});

Deregistering Hooks

// Hook registration returns a deregistration function
const deregister = transitionService.onStart({}, (transition) => {
  console.log('Hook called');
});

// Call to remove hook
deregister();

Hook Return Values

Hook return values control transition behavior:

Continue Transition

// Return nothing/undefined - continue
transitionService.onBefore({}, (transition) => {
  console.log('Logging transition');
  // No return - transition continues
});

// Return true - continue
transitionService.onBefore({}, (transition) => {
  return true;
});

Cancel Transition

// Return false - cancel transition
transitionService.onBefore({}, (transition) => {
  if (someCondition) {
    return false;  // Abort transition
  }
});

Redirect Transition

import { TargetState } from '@uirouter/core';

// Return TargetState to redirect
transitionService.onBefore({}, (transition) => {
  if (!isAuthenticated()) {
    return transition.router.stateService.target('login');
  }
});

// Return state name (string)
transitionService.onBefore({}, (transition) => {
  if (needsOnboarding()) {
    return 'onboarding';
  }
});

Async Hooks

// Return promise
transitionService.onBefore({}, async (transition) => {
  const authenticated = await checkAuth();
  
  if (!authenticated) {
    return transition.router.stateService.target('login');
  }
});

Common Hook Patterns

Authentication Hook

transitionService.onBefore(
  { to: (state) => state.data?.requiresAuth },
  async (transition) => {
    const authService = transition.injector().get('AuthService');
    const authenticated = await authService.isAuthenticated();
    
    if (!authenticated) {
      return transition.router.stateService.target('login', {}, {
        location: false  // Don't update URL for redirect
      });
    }
  }
);

Authorization Hook

transitionService.onBefore(
  { to: (state) => state.data?.roles },
  async (transition) => {
    const requiredRoles = transition.to().data.roles;
    const authService = transition.injector().get('AuthService');
    const userRoles = await authService.getUserRoles();
    
    const hasPermission = requiredRoles.some(role => 
      userRoles.includes(role)
    );
    
    if (!hasPermission) {
      return transition.router.stateService.target('unauthorized');
    }
  }
);

Logging Hook

// Log all transitions
transitionService.onStart({}, (transition) => {
  console.log('Navigating from', transition.from().name, 
              'to', transition.to().name);
});

// Log successful transitions
transitionService.onSuccess({}, (transition) => {
  const duration = transition.time() || 0;
  console.log('Transition completed in', duration, 'ms');
  
  // Analytics
  analytics.pageView(transition.to().name, {
    from: transition.from().name,
    params: transition.params()
  });
});

// Log failed transitions
transitionService.onError({}, (transition) => {
  console.error('Transition failed:', transition.error());
});

Unsaved Changes Hook

transitionService.onBefore({}, (transition) => {
  const fromState = transition.from();
  
  // Check if form has unsaved changes
  if (fromState.data?.checkUnsaved) {
    const formService = transition.injector().get('FormService');
    
    if (formService.hasUnsavedChanges()) {
      const confirmed = window.confirm(
        'You have unsaved changes. Do you want to leave?'
      );
      
      if (!confirmed) {
        return false;  // Cancel transition
      }
    }
  }
});

Loading State Hook

transitionService.onStart({}, (transition) => {
  const loadingService = transition.injector().get('LoadingService');
  loadingService.show();
});

transitionService.onFinish({}, (transition) => {
  const loadingService = transition.injector().get('LoadingService');
  loadingService.hide();
});

Page Title Hook

transitionService.onSuccess({}, (transition) => {
  const toState = transition.to();
  const pageTitle = toState.data?.pageTitle || toState.name;
  document.title = `My App - ${pageTitle}`;
});
transitionService.onSuccess({}, (transition) => {
  const breadcrumbService = transition.injector().get('BreadcrumbService');
  
  // Build breadcrumb from state path
  const breadcrumbs = transition.treeChanges().to
    .filter(node => node.state.data?.breadcrumb)
    .map(node => ({
      label: node.state.data.breadcrumb,
      state: node.state.name,
      params: node.paramValues
    }));
  
  breadcrumbService.setBreadcrumbs(breadcrumbs);
});

State-Specific Hooks

Some hooks fire for specific states:

onEnter Hook

Fires when entering a state:
transitionService.onEnter({ entering: 'dashboard' }, (transition, state) => {
  console.log('Entering state:', state.name);
  
  // Access state-specific data
  const data = state.data;
  
  // Access parameters
  const params = transition.params();
});

onExit Hook

Fires when exiting a state:
transitionService.onExit({ exiting: 'editor' }, (transition, state) => {
  console.log('Exiting state:', state.name);
  
  // Cleanup
  const editorService = transition.injector().get('EditorService');
  editorService.cleanup();
});

onRetain Hook

Fires when a state is retained (stays active):
transitionService.onRetain({ retained: 'app' }, (transition, state) => {
  console.log('State retained:', state.name);
  
  // Update only if params changed
  const fromParams = transition.params('from');
  const toParams = transition.params('to');
  
  if (fromParams.query !== toParams.query) {
    // Update search results
  }
});

Inline State Hooks

Define hooks directly in state declarations:
router.stateRegistry.register({
  name: 'analytics',
  url: '/analytics',
  
  onEnter: (transition, state) => {
    console.log('Entering analytics state');
    // Track page view
  },
  
  onExit: (transition, state) => {
    console.log('Exiting analytics state');
    // Clean up
  },
  
  onRetain: (transition, state) => {
    console.log('Analytics state retained');
  }
});

Accessing Services in Hooks

Use the transition’s injector to access services:
transitionService.onBefore({}, (transition) => {
  // Get service from injector
  const authService = transition.injector().get('AuthService');
  const userData = authService.getUserData();
  
  // Get native injector (framework-specific)
  const nativeInjector = transition.injector().native;
});

Transition Object API

The transition object provides information about the transition:
transitionService.onStart({}, (transition) => {
  // States
  const fromState = transition.from();  // Source state
  const toState = transition.to();      // Destination state
  
  // Parameters
  const params = transition.params();         // All params
  const toParams = transition.params('to');   // Destination params
  const fromParams = transition.params('from'); // Source params
  
  // Tree changes
  const changes = transition.treeChanges();
  console.log('Entering:', changes.entering);
  console.log('Exiting:', changes.exiting);
  console.log('Retained:', changes.retained);
  
  // Options
  const options = transition.options();
  console.log('Custom data:', options.custom);
  
  // Services
  const injector = transition.injector();
  const router = transition.router;
  
  // Transition ID
  const id = transition.$id;
});

Hook Priorities

Control hook execution order:
// Higher priority executes first
transitionService.onBefore({}, hookFunction, { priority: 10 });

// Lower priority executes later
transitionService.onBefore({}, anotherHook, { priority: 5 });

// Default priority is 0
transitionService.onBefore({}, defaultHook);

Error Handling

Handle errors in hooks:
transitionService.onBefore({}, async (transition) => {
  try {
    const data = await someAsyncOperation();
    return data.isValid ? true : false;
  } catch (error) {
    console.error('Hook error:', error);
    // Returning false cancels transition
    return false;
  }
});

// Global error handler
transitionService.onError({}, (transition) => {
  const error = transition.error();
  console.error('Transition error:', error);
  
  // Handle specific error types
  if (error.type === RejectType.ABORTED) {
    console.log('User cancelled transition');
  }
});

Best Practices

1
Use specific hook criteria
2
// Good - specific targeting
transitionService.onBefore({ to: 'admin.**' }, authHook);

// Avoid - runs for all transitions
transitionService.onBefore({}, authHook);
3
Return promises for async operations
4
// Good
transitionService.onBefore({}, async (transition) => {
  await checkPermissions();
});

// Avoid - not waiting for async
transitionService.onBefore({}, (transition) => {
  checkPermissions();  // Returns promise but not awaited
});
5
Use state data for hook metadata
6
// In state
data: { requiresAuth: true, roles: ['admin'] }

// In hook
transitionService.onBefore(
  { to: (state) => state.data?.requiresAuth },
  authHook
);
7
Keep hooks focused
8
// Good - single responsibility
transitionService.onBefore({}, authHook);
transitionService.onBefore({}, loggingHook);

// Avoid - multiple responsibilities
transitionService.onBefore({}, (transition) => {
  checkAuth();
  logTransition();
  validateParams();
  updateAnalytics();
});
9
Clean up in onExit hooks
10
transitionService.onExit({ exiting: 'editor' }, (transition) => {
  // Clean up resources
  clearInterval(autoSaveTimer);
  unsubscribeFromEvents();
});

API Reference

Build docs developers (and LLMs) love