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.
Sticky States and DSR
Sticky states and Deep State Redirect (formerly UI-Router Extras projects) are now official UI-Router projects.
Sticky States allows your views to remain active in the DOM, even when their state has been exited. When the state is reactivated, the DOM and state data are exactly the same as when the user exited the state. This feature can be useful to implement UI tabs where each tab is a self contained module. The user could then switch seamlessly between modules without any re-initialization.
Deep State Redirect stores the most recently activated nested state (and parameters) of a state tree. The state at the top of the tree is marked as DSR. When the user later directly activates the DSR state, they are instead redirected to the state and parameters previously stored.
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$
.
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.
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('@uirouter/angularjs').default
var app = angular.module('myApp', [uiRouter]);
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.
URL Matching order
It is possible for a URL to match more than one URL Rule.
Previously, url rules were matched in the order in which they were registered. The rule which was registered first would handle the URL change.
Now, the URL rules are sorted according to a sort function. More specific rules are preferred over less specific rules.
For details, see the release notes for 1.0.0-rc.1 (search for “URL Matching Rules”)
Encoding of path and query params
Previously, path and query parameters were encoded in the same manner, using the string
parameter type.
Now, path parameters are encoded using the path
parameter type and have slashes encoded as ~2F
.
Query parameters are encoded using the query
parameter type and do not specially encode slashes.
$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.
$urlMatcherFactory
and $urlRouter
deprecation
These two services have been consolidated and re-organized as the new $urlService
.
See the $urlService
docs
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.