Skip to content

Asynchronous Dependent Observables

SteveSanderson edited this page Aug 4, 2011 · 10 revisions

Dependent observables have to return a value synchronously. But what if you want to use a dependent observable to represent some data that you fetch using an asynchronous Ajax request?

For example, you might have a set of observables to represent query parameters, e.g.,

this.pageIndex = ko.observable(0);
this.sortColumn = ko.observable("lastName");
this.sortOrder = ko.observable("asc");

Conceptually, it makes sense to have a dependent observable to represent the result of performing a query using these parameters. After all, the result of the query is a function of the query parameters.

Manually capturing the asynchronous result

The traditional way to capture an asynchronous result is to set up a further observable - let's call it queryResults - and then use a dependent observable to detect changes on any of the query parameters and populate queryResults asynchronously as a side-effect. For example,

this.queryResults = ko.observable();
ko.dependentObservable(function() {
    // Whenever "pageIndex", "sortColumn", or "sortDirection" change, this function will re-run and issue
    // an Ajax request. When the Ajax request completes, assign the resulting value to "queryResults"
    $.ajax("someUrl", {
        data: { pageNum: this.pageIndex, sortBy: this.sortColumn, sortDirection: this.sortOrder },
        success: this.queryResults
    });
}, this);

The reason why the $.ajax call is wrapped inside a ko.dependentObservable is to ensure the request will be issued not just once when this code first runs, but also re-issued each time any query parameter changes. Automatic dependency tracking will detect the dependencies on pageIndex, sortColumn, and sortDirection and force re-evaluation when they change.

This technique works fine, but if you're doing it a lot, wouldn't it be nicer to eliminate the separate queryResults observable and just emit the output directly from your dependent observable somehow? Yes, and here's how...

Dependent Observables that return deferred results

A common mechanism for handling asynchronous operations is to have some object that represents the operation in progress:

What Task<T>, Deferrable, and $.Deferred all have in common is that they represent the present-or-future availability of some result value. They all give you a way to add a callback so you'll be notified when the result becomes available (or you'll be called back immediately if the result is already available).

So, what if your dependent observable was to return a $.Deferred object to represent an Ajax request that it issued? That technique would almost completely handle our requirements here.

The one thing that technique wouldn't handle, though, is letting you easily use regular bindings to display the result, because regular bindings don't understand $.Deferred values. To solve this, you can create a standard wrapper around a dependent observable that captures the output for any $.Deferred value and transfers it onto some other, normal observable, so you can apply bindings to that other observable in the normal way.

Here's a simple implementation:

function asyncDependentObservable(evaluator, owner) {
    var result = ko.observable();
    
    ko.dependentObservable(function() {
        // Get the $.Deferred value, and then set up a callback so that when it's done,
        // the output is transferred onto our "result" observable
        evaluator.call(owner).done(result);
    });
    
    return result;
}

You can then use this, asyncDependentObservable, in place of a regular dependent observable, and its result will appear asynchronously after any of its dependencies change. For example,

this.queryResults = asyncDependentObservable(function() {
    // Whenever "pageIndex", "sortColumn", or "sortDirection" change, this function will re-run
    return $.ajax("someUrl", {
        data: { pageNum: this.pageIndex, sortBy: this.sortColumn, sortDirection: this.sortOrder }
    });
}, this);

You can then bind queryResults to your DOM elements in the usual way. Try it out - here's a live example on jsFiddle.net

A more sophisticated implementation

What you've just seen may be perfectly sufficient in many cases, but you might want to add more functionality. For example,

  • Gracefully handling result values that are either asynchronous (e.g., $.Deferred), synchronous (regular JavaScript objects), or just null
  • Coping with out-of-order responses - ensuring that your dependent observable's value only ever represents the most-recently-requested data, even if the Ajax requests complete in a different order
  • Exposing an inProgress sub-property so you can display a "loading" indicator in your UI

Here's a more sophisticated implementation that handles all of these things:

function asyncDependentObservable(evaluator, owner) {
    var result = ko.observable(), currentDeferred;
    result.inProgress = ko.observable(false); // Track whether we're waiting for a result
    
    ko.dependentObservable(function() {
        // Abort any in-flight evaluation to ensure we only notify with the latest value
        if (currentDeferred) { currentDeferred.reject(); }
        
        var evaluatorResult = evaluator.call(owner);
        // Cope with both asynchronous and synchronous values
        if (evaluatorResult && (typeof evaluatorResult.done == "function")) { // Async
            result.inProgress(true);
            currentDeferred = $.Deferred().done(function(data) {
                result.inProgress(false);
                result(data);
            });
            evaluatorResult.done(currentDeferred.resolve);
        } else // Sync
            result(evaluatorResult);
    });
    
    return result;
}

Try it out - here's a live example on jsFiddle.net

Future possibilities

With Knockout 1.3, it will be possible to express this kind of facility as an "extender", so you could turn any dependent observable into a $.Deferred-aware one like this:

this.someData = ko.dependentObservable(function() { /* ... */ }, this).extend({ async: true });

When KO 1.3 is released, this page will be updated to show that technique.

Alternatively, if the community would find it useful, we could consider enhancing all ko.dependentObservables so they are natively aware of $.Deferred result types and automatically expose such results asynchronously.