Effectively maintaining state in AngularJS applications

ui-router makes dependency loading a breeze and effectively captures the current state of an application within the routing hierarchy.


This article has been reposted on my personal blog: www.garbl.es

Properly maintaining state without hacks can be difficult to do in AngularJS. Controllers using the route params so that they can call out to APIs when they load; services keeping track of currentThis or currentThat. If you’re not careful, your application can quickly turn into a dependency mess.

With ui-router, I like to separate my state names into two camps: nouns and verbs.

  • If the state name ends in a noun (a noun state), then the state is abstract and must resolve a dependency of the same name, e.g. if the state name is presentation, then it should resolve a presentation object, load it into $scope, and that’s that.
  • If the state name ends in a verb (a verb state), then it is a child of a noun state and uses the data loaded by the noun state(s).

By sticking to these rules, noun states are just data wrappers for verb states.

Consider the following example of an application for creating online presentations. Let’s start by loading my presentation:

$stateProvider
.state('presentation', {
abstract: true,
url: '/presentation/:presentation_id',
template: '<div ui-view></div>'
controller: function ($scope, presentation) {
$scope.presentation = presentation;
},
resolve: {
presentation: function ($stateParams, PresentationApi) {
return PresentationApi.get($stateParams.presentation_id);
}
}
});

I could have loaded the presentation directly into a view, but with this approach I’m not committing my data to a single action.

Now the verb states:

.state('presentation.show', {
url: '/',
template: '<h1>{{presentation.title}}</h1>'+
'<div ui-view=></div>'
})
.state('presentation.edit', {
url: '/edit',
template:
'<input type="text" ng-model="presentation.title">'
});

Because I’ve fetched my presentation from a higher state, $scope.presentation is the same object (in memory) for any state below presentation (including presentation.show and presentation.edit). I also enjoy the clean separation of concerns between editing and viewing my data. There’s no mucking around in the controller so that I can do both in the same view or state.

I can extend this concept further to a child state slide of my presentation.

.state(‘presentation.slide’, {
abstract: true,
url: ‘/slide/:slide_id’,
template: ‘<div ui-view></div>’,
controller: function ($scope, slide) {
$scope.slide = slide;
},
resolve: {
slide: function ($stateParams, presentation) {
return presentation.getSlide($stateParams.slide_id);
}
}
})
.state('presentation.slide.show', {
url: '/',
template: ...
})
.state('presentation.slide.edit', {
url: '/edit',
template: ...
});

You get the idea. (Also notice how I’m able to use dependencies that I’ve already resolved to resolve other dependencies.) Here’s a plunker that demonstrates the concept.

I love this approach because:

  • nothing outside of the routing knows about the current state of the application, i.e. the presentation knows that it has slides, but not which one is currently being shown.
  • the name of the current state (as defined in the route name) accurately reflects the intent of the current state, e.g. presentation.slide.edit, user.account.settings.show, or user.delete.