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.

blog comments powered by Disqus