Using jQuery Deferred.notify() To Provide Locally Cached Data
Most of the data in your application hasn't changed. Sure, things are being updated all the time; but, the large majority of the data remains the same from moment to moment. As such, it might make sense to show the user outdated, locally cached data before taking the time to go to the server for the true data. But how can we do this in a way that is really clear for your application Controllers? In jQuery 1.5, we were given Deferred objects to handle our asynchronous, server-side requests. In jQuery 1.7, we were given the notify() method as mean to report "partial results" to the calling context. Perhaps we can use this notify() method to report cached data as an intermediary result.
With a Deferred object, the three primary method calls are:
- resolve()
- reject()
- notify()
Resolve() finalizes a successful deferred result; reject() finalizes a failed deferred result; and, notify() announces "progress" data that has been made available before the request has been finalized. If we leave resolve() and reject() alone, and work only with the notify() binding, we can allow our application Controllers to differentiate and ignore cached data if and when they please. Not only does this allow us to enhance the application in a progressive manner, I think it also remains in alignment with the notify() intent.
To experiment with this concept, I've put together a Demo with a few small objects:
- friendGateway
- friendService
- controller
The friendGateway encapsulates the communication with the friend repository. In this case, it simply mocks server-request-latency by using setTimeout(). All calls to the friendGateway return a Deferred promise object.
The friendService encapsulates our business logic, communicating with the friendGateway and integrating the client-side data cache. As you will see below, the friendService returns its own Deferred promise objects that enhance the friendGateway promise with notify() functionality.
The controller, which makes calls to the friendService, then listens for both notify() and resolve() events, and updates the user interface as necessary:
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>Using Deferred.notify() To Provide Locally Cached Data</title>
<script type="text/javascript" src="../jquery-1.8.1.js"></script>
<script type="text/javascript">
// I provide access to the "remote" data store for friends.
// In order to simulate the latency of talking to a remote
// machine, this gateway will always return Promise objects
// with an artificial delay.
var friendGateway = {
// I am the mock data store.
repository: [
{
id: 1,
name: "Sarah"
},
{
id: 2,
name: "Tricia"
},
{
id: 3,
name: "Joanna"
},
{
id: 4,
name: "Kit"
},
{
id: 5,
name: "Anna"
}
],
// I return the data from the "remote" store.
get: function() {
var deferred = $.Deferred();
// Resolve the data after a short delay to mimic
// network latency.
setTimeout(
function() {
deferred.resolve( friendGateway.repository );
},
(2 * 1000)
);
// deferred.resolve( friendGateway.repository );
return( deferred.promise() );
}
};
// -------------------------------------------------- //
// -------------------------------------------------- //
// -------------------------------------------------- //
// I provide access to the friend store.
// NOTE: This makes use of the friendGateway.
var friendService = {
// I store a local version of the data when it becomes
// availalbe. This can be used to supply the calling
// context with a quick view of the data before the "true"
// data has been returned from the server.
repositoryCache: null,
// I get the list of friends.
get: function() {
// Get the data from the remote gateway. This will
// return a promise.
var request = friendGateway.get();
// Create a new deferred so that we can notify our
// calling context when the request comes back.
var deferred = $.Deferred();
// Check to see if we have a cached version of the
// data. If so, we can send it back to the calling
// context as "progress".
//
// NOTE: We're checking for state because a resolve
// value will show up before a notify value if the
// deferred is already resolved.
if (
friendService.repositoryCache &&
(request.state() === "pending")
) {
deferred.notify( friendService.repositoryCache );
}
// Bind to the request outcome so we can update the
// calling context AND the repository with the latest,
// server-provided data.
request.then(
function( response ) {
// Update our local cache with the latest data.
friendService.repositoryCache = response;
// Resolve the data.
deferred.resolve( response );
},
// If the server request is rejected, simply pass
// that onto the local request.
deferred.reject
);
// Return the locked down promise.
return( deferred.promise() );
}
};
// -------------------------------------------------- //
// -------------------------------------------------- //
// -------------------------------------------------- //
$(function() {
// I show the list view, getting the data from the
// server.
function showList() {
// Show the right view.
dom.listView.show();
dom.detailView.hide();
// Clear the list while the data is loading.
dom.list
.empty()
.append( "<li>Loading...</li>" )
;
// Load the data. The callbacks here are Resolved,
// Rejected, and Notified (respectively). In this
// case, we are using the notify callback as a way
// to provide the code with locally-cached data.
var friendRequest = friendService.get().then(
function( response ) {
// As an optimization, don't do anything
// if the list view is no longer visible. No
// need to work if we don't have to.
if ( ! dom.listView.is( ":visible" ) ) {
return;
}
// Once the "clean" data comes back from
// the server, populate the list. It doesn't
// really matter if the list has already
// been populated.
populateList( response );
// Log new data response.
console.log( "New Data:", response );
},
function( error ) {
alert( "Something went wrong!" );
},
function( cachedResponse ) {
// If there is cached data, let's quick
// populate the list, even if the data is
// a bit dirty.
populateList( cachedResponse );
// Log cached data response.
console.log( "Cached Data:", cachedResponse );
}
);
}
// I show the detail view.
function showDetail( id, name ) {
// Show the right view.
dom.listView.hide();
dom.detailView.show();
// Populate the DOm.
dom.id.text( id );
dom.name.text( name );
}
// I populate the list with given data.
function populateList( response ) {
// Clear the list before populating.
dom.list.empty();
// Iterate over the reponse, creating a list item
// for each friend in the reponse data.
$.each(
response,
function( index, friend ) {
var li = $( "<li>" );
var a = $( "<a>" );
a
.attr( "href", "#" )
.attr( "data-id", friend.id )
.text( friend.name )
;
dom.list.append(
li.append( a )
);
}
);
}
// Cache DOM elements.
var dom = {};
dom.listView = $( "div.listView" );
dom.detailView = $( "div.detailView" );
dom.list = dom.listView.find( "ul" );
dom.id = dom.detailView.find( "span.id" );
dom.name = dom.detailView.find( "span.name" );
dom.backToList = dom.detailView.find( "a.back" );
// When the user clicks an item in the list, show the
// detail for the selected friend.
dom.list.on(
"click",
"li a",
function( event ) {
// Prevent default - this is not a real link.
event.preventDefault();
// Get teh target data.
var item = $( event.target ).closest( "a" );
var id = item.attr( "data-id" );
var name = item.text();
// Show the detail view.
showDetail( id, name );
}
);
// When the user clicks the back to list link, show the
// list.
dom.backToList.click(
function( event ) {
// Prevent default - this is not a real link.
event.preventDefault();
// Take the user back to the list view.
showList();
}
);
// Start out by showing the list.
showList();
});
</script>
</head>
<body>
<h1>
Using Deferred.notify() To Provide Locally Cached Data
</h1>
<!-- BEGIN: List View. -->
<div class="listView">
<h3>
List of Friends
</h3>
<ul>
<!-- To be populated dynamically. -->
</ul>
</div>
<!-- END: List View. -->
<!-- BEGIN: Detail View. -->
<div class="detailView">
<h3>
Friend Detail
</h3>
<p>
<strong>ID:</strong> <span class="id"></span><br />
<strong>Name</strong>: <span class="name"></span>
</p>
<p>
« <a href="#" class="back">Back To List</a>
</p>
</div>
<!-- END: Detail View. -->
</body>
</html>
As you can see in the Controller, the resolve() callback and the notify() callback both do the same thing - they both update the list of friends on the page. While this might seem like a duplication of effort, the intent is to provide an improved user experience. If the list of friends isn't going to change (often), we'll be able to present a more responsive interface in which the dirtiness of the data remains mostly unseen.
And, again, because the cached data is being announced via the notify() method, the Controller can completely ignore it if it wants to.
Any duplication of effort, or additional work imposed upon the browser, brings with it a certain amount of emotional discomfort. But, we have to remember that computers are hugely powerful, performing millions upon millions of operations a second. Unless rendering your interface requires a tremendous amount of effort, I'd propose that the duplication required to render intermediary, cached data will go completely unnoticed by the user. What they will notice, however, is a snappy, responsive interface.
Want to use code from this post? Check out the license.
Reader Comments
Your article gets me close to what I am looking for, but not quite close enough. I am trying to do a series of parallel calls to a webservice and monitor progress every 1 second with a progress bar on the screen showing all progress of each parallel process.
I am also trying to get my head around the $.Deferred object.
What I need is an example that shows how to check the progress during multiple running server tasks.
In the web service, I have a session dictionary with each of the objects being processed. I want this code to get called every second to provide status to the browser, but I cannot seem to find a way to call it until the job is done. Not very interesting.
@Bob,
Oooh, that's a really interesting question. I know that jQuery provides a $.when() method which will take N-number of deferred values, and wraps them in new Promise object that is completed when each of the individual deferred items is completed (then each value get's passed to the new Promise callback as an individual argument).
I wonder if the $.when() can call .notify() when each encapsulated deferred is finished? Also, I wonder if there would be an easy way to determine *which* of the encapsulated deferred values is the one that just finished.
I'll put my thinking cap on.
@Ben,
Oh, I just realized, if you have several deferred values, then you can tap into the "done" event on each of the individual deferred values. Then you can use the $.when() to know when they are all done, and the individual deferred to know when each one is completed on its own.