Guide: UI-Router 1.0 Migration

UI-Router legacy (version 0.x.x) has been the defacto standard for flexible routing in AngularJS applications. UI-Router 1.0.0+ offers significant benefits over the legacy version.

We completely rewrote the internals of much of the UI-Router code base. This has resulted in a more robust, more flexible design, which addressed many of the community’s biggest requests and concerns.

The entire codebase has been converted to Typescript, enabling explicit API parameter types.

We’ve split out the UI-Router core code and made it framework agnostic. This allowed us to quickly implement UI-Router for Angular (2+) and UI-Router for React.


In this guide, we’ll cover the new features in UI-Router. Then, we’ll cover all known breaking changes and the new approach, or workarounds.

New Features

Route to component

In legacy UI-Router, a state’s view consisted of a template and a controller. The industry, however, has embraced a component based paradigm.

UI-Router 1.0 routes directly to an AngularJS 1.5 .component().

Read our Route to Component Guide for more.

See also the component: docs and the bindings: docs.

Lazy Loading

We took the concept of “Future State” from UI-Router Extras and baked it into UI-Router as of 1.0.0-beta.2.

A future state is a placeholder for an entire feature module. The code and state definitions for the feature module isn’t loaded until the user tries to activate the future state.

Check out the api docs for lazyLoad. We will have a guide to lazy loading coming soon.

State Hook: redirectTo

UI-Router 1.0 adds the redirectTo property to a state definition.

A common feature request has been to define the default substate of some state. When a user tries to activate that state, the Transition will be redirected to the substate found in the redirectTo: property.

This property can redirect to any state, not just substates.

This property can be a function and can return a promise for an asynchronous redirect.

See the redirectTo: docs.

View Hook: uiCanExit

We’ve added a view hook for controllers to determine if their state can be exited or not. This hook is useful to prompt for confirmation before exiting a screen with “dirty” information, for instance.

If the state that the view belongs to is going to be exited during a Transition, the hook on the controller is invoked. Since it is a Transition Hook, it supports asynchronous callbacks (you could use a ui-bootstrap confirmation $modal, or check something on the server, if needed).

See the uiCanExit docs

Dynamic Parameters

Normally, when a parameter value changes, it causes the state to be reloaded (exited and then re-entered). In contrast, when a dynamic parameter changes, it does not cause the state to be reloaded.

This can be useful, e.g., on a search state to fetch new search results without reloading the state or views.

See the ParamDeclaration docs and the uiOnParamsChanged docs.

Transition Lifecycle Hooks

Transition Hooks allow a developer to hook into the state machine Transition lifecycle when going from one state to another. The hook can modify the Transition’s outcome in various ways.

In UI-Router legacy (versions 0.x), you may have used the $stateChangeStart/$stateChangeSuccess events and resolve to handle concerns like redirects or authentication. Transition Hooks are a significant improvement over these primitives.

Unlike the events, Transition Hooks allow asynchronous processing, can access resolve data (and application services), and include first class Transition redirects.

Transition hooks can be applied only when needed. A declarative criteria object can target specific target states by name (or using wildcards), or you may provide a matching function.

See the TransitionService docs for more details on how to register hooks.

Transition Object

Instead of an endless list of parameters as in the state events of UI-Router legacy, all transition details are now encapsulated in the Transition object. You can inspect parameters, origin and destination states, resolves, and trace back transition redirections.

See the Transition docs.

Resolve policy

In UI-Router legacy, all resolves for a transition were processed at the same time when the Transition starts. This could cause problems because resolves could be fetched for a state that had not yet been entered.

UI-Router 1.0 waits to fetch a state’s resolves until that state is being entered.

However a state may declare a resolve as “EAGER” to cause it to be fetched when the Transition starts if desired.

See the resolvePolicy: docs and the ResolvePolicy docs.

UI-Router visualizer

The new Transition pipeline and hooks enabled us to create the UI-Router Visualizer.

The State Visualizer provides a graphical view of the UI-Router State Tree, highlighting the active state. A state selector allows a developer to transition between the states when developing.

The Transition Visualizer shows a timeline of all UI-Router transitions. Each Transition’s status (running/success/failure/ignored) is color coded. The details of each Transition (to state/from state/parameters/resolve data) may be inspected.

See the Visualizer project homepage for details on how to add this to your app.

Breaking changes

Despite the massive refactor, we’ve managed to retain almost 100% public API compatibility with 0.2.x.

However, certain breaking changes were necessary to enable the new design. Each known breaking change is listed here, as well as how to migrate your code.

State Change Events

All state events, (i.e. $stateChange* and friends) are now deprecated and disabled by default. Instead of listening for events, use the Transition Hook API.

Migration example 1

This example prints debug information to the console for every transition.

// Legacy example
app.run(function($rootScope, SpinnerService) {
  $rootScope.$on('$stateChangeStart', function(evt, toState, toParams, fromState, fromParams) {
    console.log("$stateChangeStart " + fromState.name + JSON.stringify(fromParams) + " -> " + toState.name + JSON.stringify(toParams));
  });
  $rootScope.$on('$stateChangeSuccess', function() {
    console.log("$stateChangeSuccess " + fromState.name + JSON.stringify(fromParams) + " -> " + toState.name + JSON.stringify(toParams));
  });
  $rootScope.$on('$stateChangeError', function() {
    console.log("$stateChangeError " + fromState.name + JSON.stringify(fromParams) + " -> " + toState.name + JSON.stringify(toParams));
  });
})

UI-Router 1.0 includes a Trace service. Call Trace.enable to enable TRANSITION tracing.

// Migrate to: UI-Router 1.0 Trace service
app.run(function($trace) {
  $trace.enable('TRANSITION');
})

Migration example 2

This example enables a graphical spinner while a transition is running.

// Legacy example
app.service('SpinnerService', function() {
  var count = 0;
  return {
    transitionStart: function() { if (++count > 0) showSpinner(); },
    transitionEnd: function() { if (--count <= 0) hideSpinner(); },
  }
});
  
app.run(function($rootScope, SpinnerService) {
  $rootScope.$on('$stateChangeStart', function() {
    SpinnerService.transitionStart();
  });
  $rootScope.$on('$stateChangeSuccess', function() {
    SpinnerService.transitionEnd();
  });
  $rootScope.$on('$stateChangeError', function() {
    SpinnerService.transitionEnd();
  });
})

The Transition object has a promise property which is resolved or rejected when the transition is complete. This helps to keep parity between the start and end of an individual transition.

// Migrate to: UI-Router 1.0 Transition Hook
app.run(function($transitions) {
  $transitions.onStart({ }, function(trans) {
    var SpinnerService = trans.injector().get('SpinnerService');
    SpinnerService.transitionStart();
    trans.promise.finally(SpinnerService.transitionEnd);
  });
})

Migration example 3

This example shows a “require authentication” workflow. If the state change (Transition) is going to auth.*, synchronously check if the user is authenticated. If they are not, force the user to log in.

// Legacy example
app.run(function($rootScope, AuthService, $state) {
  $rootScope.$on('$stateChangeStart', function(evt, toState, toParams, fromState, fromParams) {
    if (toState.name.indexOf("auth.") === 0) {
      if (!AuthService.isAuthenticated()) {
        evt.preventDefault();
        $state.go('login');
      }
  });
})
// Migrate to: UI-Router 1.0 Transition Hook
app.run(function($transitions) {
  $transitions.onStart({ to: 'auth.**' }, function(trans) {
    var auth = trans.injector().get('AuthService')
    if (!auth.isAuthenticated()) {
      // User isn't authenticated. Redirect to a new Target State
      return trans.router.stateService.target('login');
    }
  });
})

$stateChange event polyfill

To ease your migration, we’ve included a polyfill that re-enables the $stateChangeStart angular events. The polyfill is implemented as a Transition Hook.

To use the polyfill, include the release/stateEvents.js file, then add an angular module dependency on ui.router.state.events in your application.

See Deprecated State Events documentation for more details.

onEnter/onExit return value

Return values for onEnter/onExit hooks are now meaningful.

An onEnter/onExit declared on a state is processed as a standard Transition Hook. The return value of a hook may alter a transition.

To be safe, “implicit return” users (i.e., ES6 arrow functions or CoffeeScript) should either add an explicit return to the onEnter/onExit expression, or to stop it from returning a meaningful value.

.state('foo', {
  // this arrow function's return value would be processed.
  // it might have returned 'false' and cancel the transition,
  // or returned some promise and delayed the transition. oops.
  // onEnter: (MyService) => MyService.doThing() 
  
  // Change from an arrow expression to a block by wrapping in { }.
  // The arrow no longer returns a value.  nice.
  onEnter: (MyService) => { MyService.doThing(); }
});   

Note: Normally, global State Hook Functions are not injected. They always receive the Transition as the first argument and the State context as the second argument.

However, for backwards compatibility in AngularJS (1.x) only, onEnter / onRetain / onExit hooks that are declared on a state are still injected, just as in legacy code.

To inject the Transition, use $transition$. To inject the State context, use $state$.

CommonJS module name

Because UI-Router uses ES6 exports (via Typescript), CommonJS users can no longer require the module name using the default export.

We recommend either using an ES6 default import, or switching your require to this:

var uiRouter = require('angular-ui-router').default
var app = angular.module('myApp', [uiRouter]);

NPM package name changed

We switched to scoped npm packages! This means instead of installing angular-ui-router you should install @uirouter/angularjs.

Read more about scoped packages on our blog.

UMD Bundle changes

We continue to support a single UMD bundle (angular-ui-router.js) to help with migration, plunkers, and backwards compatibility.

However, we split out the core of UI-Router to its own project (@uirouter/core). UI-Router core is published as a separate project on its own release schedule.

Due to this change, we recommend switching from angular-ui-router.js to the modular UMD bundles. The modular bundles are also required enable most UI-Router plugins such as sticky states and deep state redirect.

The new modular UMD bundle (ui-router-angularjs.js) does not bundle the UI-Router Core code. The UI-Router Core code can be found in the @uirouter/core releases as ui-router-core.js. You should use the version of @uirouter/core that the @uirouter/angularjs package depends on (and use npm or bower to manage that).

Note: In general, this affects users who use <script> tags, and users of SystemJS. Webpack users should not be affected because webpack reads the metadata inside the package.json when bundling.

Non-Optional Path Parameters

In UI-Router legacy, a parameter found in the URL path was optional. If the parameter was not found in the URL, it would match the empty string.

In UI-Router 1.0, parameters are non-optional by default. If you try to navigate to a URL with a missing path parameter, you’ll get an error message: "Param values not valid for state 'foo'"

To make a path parameter optional again, specify a default value.

$stateProvider.state('foo', {
  url: '/foo/:someOptionalParam',
  params: {
    someOptionalParam: ""
  }
});

See the params: docs and ParamDeclaration docs

States with parent: property

States can specify their parent using the name property such as name: 'parentState.childState'. Alternatively, they can specify a parent explicitly using the parent property like parent: 'parentState'.

We now throw an error if the state specifies a parent using BOTH mechanisms, because it is ambiguous.

$stateParams deprecation

In 1.0, we recommend injecting the $transition$ object instead. You can get and get the current transition’s parameters using $transition$.params().

The injectable $stateParams object has always been confusing. When it is injected into a service, the service receives a global object, which is updated whenever a transition completes. However, when injected into a state’s controller, the controller received a completely different object, which contains the parameter values for a pending transition. Additionally, those values are filtered to the parameters that the state owns.

LAZY resolves

In UI-Router legacy, all resolves were fetched ASAP when the transition started. Now, resolves are LAZY by default, which means they are not fetched until their state is being Entered.

This means that resolves for parent states are fetched before resolves for child states.

To make a resolve fetch when the transition starts, declare it as an EAGER resolve.

$stateProvider.state('foo.bar.baz', {
  resolve: {
    myEagerResolve: function(MyService) {
      return MyService.fetchThings();
    }
  },
  resolvePolicy: {
    myEagerResolve: { when: 'EAGER' }
  }
});

See the resolvePolicy: docs.

Default Error Handler

In UI-Router legacy, users often complained that resolves would “swallow errors”. The errors were output as a $stateChangeError event, but applications sometimes didn’t listen to the event.

In UI-Router 1.0, we’ve added a default error handler. When a call to $state.go() fails, we pass the error to StateService.defaultErrorHandler.

The built-in defaultErrorHandler prints the error to the console. Because of this, errors that were silent in your legacy app may start making some noise in the console.

To revert this behavior, or customize it, provide your own custom defaultErrorHandler implementation.

app.run(function($state) {
  window.myAppErrorLog = [];
  $state.defaultErrorHandler(function(error) {
    // This is a naive example of how to silence the default error handler.
    window.myAppErrorLog.push(error);
  });
})

resolve inside views

We no longer process resolve blocks that are declared inside a views.

We used to allow resolves to be declared in a view:

.state('foo', {
  views: {
    foo: { 
      template: '<h1>foo</h1>',
      resolve: { fooData: () => 'fooData' },
    },
    bar: { 
      template: '<h1>bar</h1>',
      resolve: { barData: () => 'barData' },
    }
  }
})

However, those resolves were never actually scoped to the views. They were scoped to the state itself, which could lead to undefined behavior

Now, move all resolves out to a resolve block on the state:

.state('foo', {
  views: {
    foo: { template: '<h1>foo</h1>' },
    bar: { template: '<h1>bar</h1>' },
  },
  resolve: { 
    fooData: () => 'fooData',
    barData: () => 'barData',
  },
})

Prototypal inherited .data

This is technically a breaking change in 0.2.16, but is worth mentioning. The State .data property inherits data from its parent state(s).

In 0.2.15 and prior, properties from the parent state’s data object were copied to the child state’s data object when the state was registered. If the parent state’s data object changed at runtime, the children didn’t reflect the change.

In 0.2.16+, we switched to prototypal inheritance. Changes to the parent data object are reflected in the child.

However, you can no longer test if the data object has a parent state property using dataObj.hasOwnProperty().

// Legacy code can check for hasOwnProperty
app.run(function ($rootScope) {
  $rootScope.$on('$stateChangeStart', function(evt, toState) {
    if (!toState.data) return;
    if (toState.data.hasOwnProperty('auth') && toState.data.auth === true) {
      // do authy stuff
    }
    Object.keys(toState.data, function(key) {
      console.log("data key: " + key + " = " + toState.data[key]);
    });
  });
});
// 0.2.16+ can't use hasOwnProperty to check for parent properties
app.run(function ($rootScope) {
  $rootScope.$on('$stateChangeStart', function(evt, toState) {
    if (!toState.data) return;
    // Access directly
    if (toState.data.auth === true) {
      // do authy stuff
    }
    // Prototypal properties not enumerable using Object.keys()
    for (var key in toState.data) {
      console.log("data key: " + key + " = " + toState.data[key]);
    });
  });
});

Removed notify transition option

In legacy, notify: false could be used to suppress the $stateChangeStart and other state events.

This was often used to modify the URL without performing a full state change. However, this left the state machine with undefined behavior.

We introduced dynamic parameters as a mechanism to update parameters (and update the parameters in the URL) without reloading (exiting/entering) the state.

Resolve this binding

Resolves no longer have this bound to the state object.

No workaround provided.

Dropping support for IE8

No workaround provided.