A Better $state.reload for the AngularJS UI-Router
While working on Apigility, several times I ran into an odd issue: after fetching new data via an API and assigning it to a scoped variable, content would flash into existence… and then disappear. Nothing would cause it to display again other than a browser reload of the page.
Setup
I have a page that lists a set of items. When you create an item, you push data to the API, and, when done, the new item should be in that list.
First try: append to list
My first attempt was just appending the data to the list.
service.create(data).then(function (newItem) {
flash.success = 'Successfully created something';
/* append new item to list */
$scope.services.push(newItem);
});
This worked… until you left that screen and returned. At that point, the new item would be gone, even if I coded my ui-router states to force a cache refresh.
Refresh list
My next attempt was to write a routine that would do a cache refresh after creating the new item.
service.create(data).then(function (newItem) {
flash.success = 'Successfully created something';
service.fetchAll(var force = true).then(function (services) {
$scope.services = services;
});
});
This is when I started noticing the "flash of content" problem. Essentially, immediately after fetching the set of services, you'd see the new item appended… and then it would disappear.
$state.reload()
At this point, I figured I'd use the ui-router to force a refresh, specifically
via $state.reload()
.
service.create(data).then(function (newItem) {
flash.success = 'Successfully created something';
service.fetchAll(var force = true).then(function (services) {
$scope.services = services;
$state.reload();
});
});
I tried both with and without setting the scoped variable. Initially, I thought it was working — but, as it turned out, I missed a case. I tested every single time with at least one item already in the list — and this approach worked. However, when I tried with the list not yet populated, failure once again.
Success: $timeout
Surprisingly, the least intuitive solution ended up working: introducing a delay.
service.create(data).then(function (newItem) {
flash.success = 'Successfully created something';
service.fetchAll(var force = true)
.then(function (services) {
$scope.services = services;
}).then(function () {
return $timeout(function () {
$state.go('.', {}, { reload: true });
}, 100);
});
});
I have a few things to note about this. First, I moved the "reload" into its
own promise. This was done to ensure it doesn't block on the scope assignment.
Second, I introduce a $timeout
call. This essentially gives the scope a
chance to populate before the reload triggers. Some examples I saw did a 1ms
timeout; I found in practice that this was not long enough; 100ms was long
enough, and did not introduce a noticeable delay in UI responsiveness. Finally,
you'll note this does not use $state.reload()
. This is due to discovering
that part of my problem is a known bug in $state.reload()
,
whereby state "resolve" configuration is not honored.
I hope this approach helps others — I've found it to be robust and predictable.