Index: wp-admin/js/theme.js
===================================================================
--- wp-admin/js/theme.js	(revision 27821)
+++ wp-admin/js/theme.js	(working copy)
@@ -213,6 +213,123 @@
 		collection = _( collection.first( 20 ) );
 
 		return collection;
+	},
+
+	// Handles requests for more themes
+	// and caches results
+	//
+	// When we are missing a cache object we fire an apiCall()
+	// which triggers events of `query:success` or `query:fail`
+	query: function( request ) {
+		/**
+		 * @static
+		 * @type Array
+		 */
+		var queries = this.queries,
+			self = this,
+			query, isPaginated;
+
+		// Search the query cache for matches.
+		query = _.find( queries, function( query ) {
+			return _.isEqual( query.request, request );
+		});
+
+		// If the request matches the stored currentQuery.request
+		// it means we have a paginated request.
+		isPaginated = _.has( request, 'page' );
+
+		// Reset the internal api page counter for non paginated queries.
+		if ( ! isPaginated ) {
+			this.currentQuery.page = 1;
+		}
+
+		// Otherwise, send a new API call and add it to the cache.
+		if ( ! query ) {
+			query = this.apiCall( request ).done( function( data ) {
+				// Update the collection with the queried data.
+				self.reset( data.themes );
+
+				// Trigger a `query:success` event
+				// and a collection refresh event so the views are aware of changes.
+				self.trigger( 'query:success' );
+				self.trigger( 'update' );
+
+				// Store the results and the query request
+				queries.push( { themes: data.themes, request: request } );
+			}).fail( function() {
+				self.trigger( 'query:fail' );
+			});
+		} else {
+			// If it's a paginated request we need to fetch more themes...
+			if ( isPaginated ) {
+				return this.apiCall( request, isPaginated ).done( function( data ) {
+					// Add the new themes to the current collection
+					// @todo update counter
+					self.add( data.themes );
+					self.trigger( 'query:success' );
+
+				}).fail( function() {
+					self.trigger( 'query:fail' );
+				});
+			}
+
+			// Only trigger an update event since we already have the themes
+			// on our cached object
+			this.reset( query.themes );
+			this.trigger( 'update' );
+		}
+	},
+
+	// Local cache array for API queries
+	queries: [],
+
+	// Keep track of current query so we can handle pagination
+	currentQuery: {
+		page: 1,
+		request: {}
+	},
+
+	// Send Ajax POST request to api.wordpress.org/themes
+	apiCall: function( request, paginated ) {
+		// Store current query request args
+		// for later use with the event `theme:end`
+		this.currentQuery.request = request;
+
+		// Ajax request to .org API
+		return $.ajax({
+			url: 'https://api.wordpress.org/themes/info/1.1/?action=query_themes',
+
+			// We want JSON data
+			dataType: 'json',
+			type: 'POST',
+			crossDomain: true,
+
+			// Request data
+			data: {
+				action: 'query_themes',
+				request: _.extend({
+					per_page: 72,
+					fields: {
+						description: true,
+						tested: true,
+						requires: true,
+						rating: true,
+						downloaded: true,
+						downloadLink: true,
+						last_updated: true,
+						homepage: true,
+						num_ratings: true
+					}
+				}, request)
+			},
+
+			beforeSend: function() {
+				if ( ! paginated ) {
+					// Spin it
+					$( 'body' ).addClass( 'loading-themes' );
+				}
+			}
+		});
 	}
 });
 
@@ -628,7 +745,10 @@
 
 		// Generate the themes
 		// Using page instance
-		this.renderThemes( this.parent.page );
+		// While checking the collection has items
+		if ( this.options.collection.size() > 0 ) {
+			this.renderThemes( this.parent.page );
+		}
 
 		// Display a live theme count for the collection
 		this.count.text( this.collection.length );
@@ -642,7 +762,9 @@
 		self.instance = self.collection.paginate( page );
 
 		// If we have no more themes bail
-		if ( self.instance.length === 0 ) {
+		if ( self.instance.size() === 0 ) {
+			// Fire a no-more-themes event.
+			this.parent.trigger( 'theme:end' );
 			return;
 		}
 
@@ -967,7 +1089,7 @@
 		_.debounce( _.bind( this.doSearch, this ), 300 )( event.target.value );
 	},
 
-	doSearch: function( value ) {
+	doSearch: _.debounce( function( value ) {
 		var request = {},
 			self = this;
 
@@ -991,23 +1113,10 @@
 			request.tag = [ value.slice( 4 ) ];
 		}
 
-		// Send Ajax POST request to api.wordpress.org/themes
-		themes.view.Installer.prototype.apiCall( request ).done( function( data ) {
-				// Update the collection with the queried data
-				self.collection.reset( data.themes );
-				// Trigger a collection refresh event to render the views
-				self.collection.trigger( 'update' );
-
-				// Un-spin it
-				$( 'body' ).removeClass( 'loading-themes' );
-				$( '.theme-browser' ).find( 'div.error' ).remove();
-		}).fail( function() {
-				$( '.theme-browser' ).find( 'div.error' ).remove();
-				$( '.theme-browser' ).append( '<div class="error"><p>' + l10n.error + '</p></div>' );
-		});
-
-		return false;
-	}
+		// Get the themes by sending Ajax POST request to api.wordpress.org/themes
+		// or searching the local cache
+		this.collection.query( request );
+	}, 300 ),
 });
 
 themes.view.Installer = themes.view.Appearance.extend({
@@ -1022,62 +1131,33 @@
 		'click [type="checkbox"]': 'addFilter'
 	},
 
-	// Send Ajax POST request to api.wordpress.org/themes
-	apiCall: function( request ) {
-		return $.ajax({
-			url: 'https://api.wordpress.org/themes/info/1.1/?action=query_themes',
-
-			// We want JSON data
-			dataType: 'json',
-			type: 'POST',
-			crossDomain: true,
-
-			// Request data
-			data: {
-				action: 'query_themes',
-				request: _.extend({
-					per_page: 36,
-					fields: {
-						description: true,
-						tested: true,
-						requires: true,
-						rating: true,
-						downloaded: true,
-						downloadLink: true,
-						last_updated: true,
-						homepage: true,
-						num_ratings: true
-					}
-				}, request)
-			},
-
-			beforeSend: function() {
-				// Spin it
-				$( 'body' ).addClass( 'loading-themes' );
-			}
-		});
-	},
-
 	// Handles all the rendering of the public theme directory
 	browse: function( section ) {
 		var self = this;
 
-		// @todo Cache the collection after fetching based on the section
 		this.collection = new themes.Collection();
 
-		// Create a new collection with the proper theme data
-		// for each section
-		this.apiCall({ browse: section }).done( function( data ) {
-			// Update the collection with the queried data
-			self.collection.reset( data.themes );
-			// Trigger a collection refresh event to render the views
-			self.collection.trigger( 'update' );
+		// Bump `collection.currentQuery.page` and request more themes if we hit the end of the page.
+		this.listenTo( this, 'theme:end', function() {
+			self.collection.currentQuery.page++;
+			_.extend( self.collection.currentQuery.request, { page: self.collection.currentQuery.page } );
+			self.collection.query( self.collection.currentQuery.request );
+		});
 
-			// Un-spin it
+		this.listenTo( this.collection, 'query:success', function() {
 			$( 'body' ).removeClass( 'loading-themes' );
 			$( '.theme-browser' ).find( 'div.error' ).remove();
 		});
 
+		this.listenTo( this.collection, 'query:fail', function() {
+			$( '.theme-browser' ).find( 'div.error' ).remove();
+			$( '.theme-browser' ).append( '<div class="error"><p>' + l10n.error + '</p></div>' );
+		});
+
+		// Create a new collection with the proper theme data
+		// for each section
+		this.collection.query( { browse: section } );
+
 		if ( this.view ) {
 			this.view.remove();
 		}
@@ -1091,6 +1171,7 @@
 
 		// Reset pagination every time the install view handler is run
 		this.page = 0;
+		this.collection.currentQuery.page = 1;
 
 		// Render and append
 		this.$el.find( '.themes' ).remove();
@@ -1153,27 +1234,12 @@
 
 		// Construct the filter request
 		// using the default values
-
-		// @todo Cache the collection after fetching based on the filter
 		filter = _.union( filter, this.filtersChecked() );
 		request = { tag: [ filter ] };
 
-		// Send Ajax POST request to api.wordpress.org/themes
-		this.apiCall( request ).done( function( data ) {
-				// Update the collection with the queried data
-				self.collection.reset( data.themes );
-				// Trigger a collection refresh event to render the views
-				self.collection.trigger( 'update' );
-
-				// Un-spin it
-				$( 'body' ).removeClass( 'loading-themes' );
-				$( '.theme-browser' ).find( 'div.error' ).remove();
-		}).fail( function() {
-				$( '.theme-browser' ).find( 'div.error' ).remove();
-				$( '.theme-browser' ).append( '<div class="error"><p>' + l10n.error + '</p></div>' );
-		});
-
-		return false;
+		// Get the themes by sending Ajax POST request to api.wordpress.org/themes
+		// or searching the local cache
+		this.collection.query( request );
 	},
 
 	// Clicking on a checkbox triggers a tag request
@@ -1182,20 +1248,9 @@
 			tags = this.filtersChecked(),
 			request = { tag: tags };
 
-		// Send Ajax POST request to api.wordpress.org/themes
-		this.apiCall( request ).done( function( data ) {
-				// Update the collection with the queried data
-				self.collection.reset( data.themes );
-				// Trigger a collection refresh event to render the views
-				self.collection.trigger( 'update' );
-
-				// Un-spin it
-				$( 'body' ).removeClass( 'loading-themes' );
-				$( '.theme-browser' ).find( 'div.error' ).remove();
-		}).fail( function() {
-				$( '.theme-browser' ).find( 'div.error' ).remove();
-				$( '.theme-browser' ).append( '<div class="error"><p>' + l10n.error + '</p></div>' );
-		});
+		// Get the themes by sending Ajax POST request to api.wordpress.org/themes
+		// or searching the local cache
+		this.collection.query( request );
 	},
 
 	// Get the checked filters and return an array
