Skip to main content

Overview

Transition hooks allow you to tap into the transition lifecycle. Hooks are registered with match criteria to control when they fire, and can return values to control transition behavior.

HookMatchCriteria

Defines which transitions should invoke a hook.
interface HookMatchCriteria {
  to?: HookMatchCriterion;
  from?: HookMatchCriterion;
  entering?: HookMatchCriterion;
  exiting?: HookMatchCriterion;
  retained?: HookMatchCriterion;
}
All properties are optional. Omitted properties default to true (match all).

HookMatchCriterion

Each criterion can be:
  1. String - Glob pattern matching state name
  2. Boolean - true matches all, false matches none
  3. Function - Custom predicate: (state, transition) => boolean
type HookMatchCriterion = string | boolean | IStateMatch;
type IStateMatch = (state: StateObject, transition: Transition) => boolean;

Match Criteria Examples

Basic State Matching

// Match transition to specific state
{ to: 'parent.child' }

// Match transition from specific state
{ from: 'parent.child' }

Glob Patterns

// Match any substate of parent
{ to: 'parent.**' }

// Match parent or any substate
{ to: 'parent.*' }

// Match any state in mymodule
{ to: 'mymodule.**' }

Multiple Criteria

// Match specific from/to combination
{
  from: 'parent.child',
  to: 'parent'
}

// Match coming from any parent substate to parent
{
  from: 'parent.**',
  to: 'parent'
}

Function Predicates

// Match states with specific data
{
  to: (state) => state.data?.authRequired === true
}

// Match based on state properties
{
  to: (state) => state.data?.requiresPremium === true
}

// Match using transition context
{
  from: (state, transition) => {
    return state.self.name === '' || transition.options().reload;
  }
}

Entering/Exiting/Retained

// Match when entering specific state
{ entering: 'admin' }

// Match when exiting specific state
{ exiting: 'editor' }

// Match when retaining specific state
{ retained: 'parent' }

Complex Example

// Match transitioning to admin from non-admin, when entering admin state
{
  to: 'admin.**',
  from: (state) => !state.name.startsWith('admin'),
  entering: 'admin'
}

HookRegOptions

Options for configuring hook registration.
interface HookRegOptions {
  priority?: number;
  bind?: any;
  invokeLimit?: number;
}

priority

Controls hook execution order within a phase. Higher priority = earlier execution.
// This runs before default priority (0)
transitionService.onBefore({}, callback, { priority: 100 });

// This runs after default priority
transitionService.onBefore({}, callback, { priority: -100 });
Default: 0 Built-in priorities:
  • Eager resolve: high priority
  • Lazy resolve: default priority
  • View loading: specific priorities

bind

Sets the this context for the hook callback.
class MyService {
  check() { return this.isReady; }
  
  registerHook(transitionService) {
    transitionService.onBefore({}, this.check, {
      bind: this
    });
  }
}

invokeLimit

Automatically deregisters hook after N invocations.
// Run only once
transitionService.onSuccess({}, callback, { invokeLimit: 1 });

// Run 5 times then deregister
transitionService.onStart({}, callback, { invokeLimit: 5 });

HookResult

The return value from a hook function controls transition behavior.
type HookResult = boolean | TargetState | void | Promise<boolean | TargetState | void>;

Return Values

false - Cancel Transition

transitionService.onBefore({}, (trans) => {
  if (!isValid()) {
    return false; // Cancels transition
  }
});

TargetState - Redirect Transition

transitionService.onBefore({ to: 'admin.**' }, (trans) => {
  if (!isAuthenticated()) {
    // Redirect to login
    return trans.router.stateService.target('login', {
      returnTo: trans.to().name
    });
  }
});

Promise - Async Hook

transitionService.onStart({}, (trans) => {
  // Transition waits for promise
  return fetchUserData().then(user => {
    if (!user.active) {
      return false; // Cancel if inactive
    }
    // Continue transition if active
  });
});

void / undefined / true - Continue Transition

transitionService.onEnter({}, (trans) => {
  analytics.track('pageview');
  // Return nothing = continue
});

Promise Resolution

When a hook returns a Promise:
  • Resolves to false → Transition cancelled
  • Resolves to TargetState → Transition redirected
  • Resolves to anything else → Transition continues
  • Rejects → Transition cancelled with error
transitionService.onBefore({}, async (trans) => {
  try {
    const canProceed = await checkPermission();
    if (!canProceed) {
      return trans.router.stateService.target('forbidden');
    }
  } catch (error) {
    // Promise rejection cancels transition
    throw new Error('Permission check failed');
  }
});

Hook Function Signatures

TransitionHookFn

For onBefore, onStart, onFinish, onSuccess, onError.
interface TransitionHookFn {
  (transition: Transition): HookResult;
}
Example:
function myHook(transition: Transition): HookResult {
  const toState = transition.to();
  const params = transition.params();
  
  if (toState.data?.requiresAuth && !isAuthenticated()) {
    return transition.router.stateService.target('login');
  }
}

transitionService.onBefore({}, myHook);

TransitionStateHookFn

For onEnter, onExit, onRetain.
interface TransitionStateHookFn {
  (transition: Transition, state: StateDeclaration): HookResult;
}
Example:
function myStateHook(
  transition: Transition,
  state: StateDeclaration
): HookResult {
  console.log(`Entering state: ${state.name}`);
  
  // Access state-specific data
  if (state.data?.logAnalytics) {
    analytics.track(state.name);
  }
}

transitionService.onEnter({ entering: 'dashboard' }, myStateHook);

TransitionCreateHookFn

For onCreate (plugin use only).
interface TransitionCreateHookFn {
  (transition: Transition): void;
}

Hook Phases

Hooks execute in specific phases during the transition lifecycle.
enum TransitionHookPhase {
  CREATE,   // Transition being created
  BEFORE,   // Before transition starts
  RUN,      // Main transition execution
  SUCCESS,  // After successful completion
  ERROR     // After transition fails
}

Phase Details

PhaseHooksCan Cancel?Can Redirect?Async?
CREATEonCreate
BEFOREonBefore
RUNonStart, onExit, onRetain, onEnter, onFinish
SUCCESSonSuccess
ERRORonError

RUN Phase Ordering

Within the RUN phase, hooks execute in this order:
  1. onStart (priority 0)
  2. onExit (priority 100, reverse order - children first)
  3. onRetain (priority 200)
  4. onEnter (priority 300, parent to child)
  5. onFinish (priority 400)

Hook Scope

Hooks have different scopes determining what they match against.
enum TransitionHookScope {
  TRANSITION,  // Matches transition endpoints only
  STATE        // Matches all states in path
}

TRANSITION Scope

Matches only the tail of the path (final state). Paths with TRANSITION scope:
  • to - destination state
  • from - source state
Example:
// Only matches if transitioning TO 'admin'
{ to: 'admin' }

// Doesn't match transitioning to 'admin.users'

STATE Scope

Matches each state in the path. Paths with STATE scope:
  • entering - each state being entered
  • exiting - each state being exited
  • retained - each state being retained
Example:
// Matches when entering 'admin', even if going to 'admin.users'
{ entering: 'admin' }

// Hook fires for BOTH admin and admin.users states

Advanced Patterns

Conditional Default Substate

transitionService.onBefore(
  {
    to: (state) => state.defaultSubstate != null
  },
  (trans) => {
    const substate = trans.to().defaultSubstate;
    return trans.router.stateService.target(substate);
  }
);

Lazy Loading Module

transitionService.onBefore(
  { to: 'admin.**' },
  async (trans) => {
    if (!isModuleLoaded('admin')) {
      await loadModule('admin');
    }
  }
);

Parameter Validation

transitionService.onBefore(
  { to: 'user' },
  (trans) => {
    const userId = trans.params().userId;
    if (!isValidUserId(userId)) {
      return trans.router.stateService.target('404');
    }
  }
);

Unsaved Changes Guard

transitionService.onBefore(
  { from: 'editor' },
  (trans) => {
    const editor = trans.injector(null, 'from').get('EditorService');
    
    if (editor.hasUnsavedChanges()) {
      return editor.confirmDiscard().then(confirmed => {
        return confirmed ? true : false;
      });
    }
  }
);

Role-Based Access Control

transitionService.onBefore(
  {
    to: (state) => state.data?.requiredRoles != null
  },
  (trans) => {
    const requiredRoles = trans.to().data.requiredRoles;
    const auth = trans.injector().get('AuthService');
    
    if (!auth.hasAnyRole(requiredRoles)) {
      return trans.router.stateService.target('forbidden');
    }
  }
);

Analytics Tracking

transitionService.onSuccess({}, (trans) => {
  analytics.track('pageview', {
    page: trans.to().name,
    params: trans.params(),
    from: trans.from().name,
    duration: trans.$id // or actual duration
  });
});

Deregistration

All hook registration methods return a deregistration function.
const deregister = transitionService.onBefore({}, callback);

// Remove hook
deregister();

Auto-deregistration

// Using invokeLimit
transitionService.onBefore({}, callback, { invokeLimit: 1 });
// Automatically deregistered after first invocation

// Manual cleanup
const dereg = transitionService.onBefore({}, callback);
scope.$on('$destroy', dereg); // Angular 1 example

TypeScript Definitions

Complete Type Reference

// Hook registration function type
type IHookRegistration = (
  matchCriteria: HookMatchCriteria,
  callback: HookFn,
  options?: HookRegOptions
) => Function;

// All hook function types
type HookFn = 
  | TransitionHookFn 
  | TransitionStateHookFn 
  | TransitionCreateHookFn;

// Hook result type
type HookResult = 
  | boolean 
  | TargetState 
  | void 
  | Promise<boolean | TargetState | void>;

// Match criterion type
type HookMatchCriterion = 
  | string 
  | boolean 
  | ((state: StateObject, transition: Transition) => boolean);

See Also

Build docs developers (and LLMs) love