Knockout.js Observable Arrays — Reactive List Management
In this tutorial, you will learn about Knockout.js Observable Arrays. We cover key concepts, practical examples, and best practices to help you master this topic.
Knockout.js observableArray is a reactive array wrapper that tracks additions, removals, and reorderings, enabling automatic UI updates whenever the underlying list changes.
What You'll Learn
- Creating observable arrays with initial data
- Using observableArray methods vs native array methods
- Tracking array changes with subscribable events
- Filtering, sorting, and paginating arrays
- Performance best practices for large lists
Why It Matters
Lists are everywhere in web applications: product catalogs, news feeds, chat messages, to-do items. Observable arrays let you manipulate the list with simple method calls while the UI updates automatically, removing entire categories of bugs.
Real-World Use
A real-time collaboration board where team members add, reorder, and delete sticky notes. Each note is an item in an observableArray, and changes from other users arrive via Websocket and are pushed into the array, instantly updating every connected client's view.
ObservableArray Notification Flow
flowchart LR
A[observableArray.push(item)] --> B[Array Mutation]
B --> C[Notify Subscribers]
C --> D[foreach Binding Re-renders]
C --> E[Computed Re-evaluates]
C --> F[Custom Subscription Fires]
style B fill:#e6f3ff,stroke:#4a90d9,stroke-width:2px
Creating an Observable Array
// Empty array
var items = ko.observableArray();
// Array with initial items
var fruits = ko.observableArray(['apple', 'banana', 'cherry']);
// Array of objects
var users = ko.observableArray([
{ name: 'Alice', age: 30 },
{ name: 'Bob', age: 25 },
{ name: 'Charlie', age: 35 }
]);
ObservableArray Methods
Always use built-in methods to ensure change notification:
var list = ko.observableArray(['a', 'b', 'c']);
// Adding items
list.push('d'); // Adds to end: ['a','b','c','d']
list.unshift('z'); // Adds to front: ['z','a','b','c','d']
// Removing items
list.pop(); // Removes last: ['z','a','b','c']
list.shift(); // Removes first: ['a','b','c']
// Add/remove at index
list.splice(1, 1); // Removes 1 item at index 1: ['a','c']
list.splice(1, 0, 'b'); // Inserts 'b' at index 1: ['a','b','c']
// Reordering
list.sort(); // Sorts alphabetically
list.reverse(); // Reverses order
// Replacing all items
list(['x', 'y', 'z']); // Completely replaces array
// Getting the underlying array
var raw = list(); // Returns plain JavaScript array
console.log(raw.length); // Output: 3
Expected output: Each method call both modifies the underlying array and notifies subscribers, triggering UI updates for any foreach or template binding bound to the array.
Subscribing to Array Changes
Use subscribe to react to any change:
var tasks = ko.observableArray();
tasks.subscribe(function(newArray) {
console.log('Tasks updated. Count: ' + newArray.length);
});
// More granular tracking with beforeRemove and afterAdd
tasks.subscribe(function(changes) {
changes.forEach(function(change) {
if (change.status === 'added') {
console.log('Added: ' + change.value.name);
}
if (change.status === 'deleted') {
console.log('Removed: ' + change.value.name);
}
});
}, null, 'arrayChange');
Expected output: The arrayChange subscription provides detailed add/remove notifications with the specific items that changed, enabling animated add/remove transitions.
Filtering and Sorting with Computeds
Derive filtered/sorted views without modifying the original array:
function ProductListViewModel() {
var self = this;
self.allProducts = ko.observableArray([
{ name: 'Laptop', price: 999, category: 'electronics' },
{ name: 'Shirt', price: 29, category: 'clothing' },
{ name: 'Phone', price: 699, category: 'electronics' },
{ name: 'Jeans', price: 79, category: 'clothing' }
]);
self.searchQuery = ko.observable('');
self.selectedCategory = ko.observable('all');
self.sortAscending = ko.observable(true);
self.filteredProducts = ko.pureComputed(function() {
var products = self.allProducts();
var query = self.searchQuery().toLowerCase();
var category = self.selectedCategory();
// Filter by category
if (category !== 'all') {
products = products.filter(function(p) { return p.category === category; });
}
// Filter by search query
if (query) {
products = products.filter(function(p) {
return p.name.toLowerCase().indexOf(query) !== -1;
});
}
// Sort by price
products = products.slice().sort(function(a, b) {
return self.sortAscending() ? a.price - b.price : b.price - a.price;
});
return products;
});
}
Expected output: As the user types in the search box or changes the category filter, filteredProducts re-evaluates and the foreach binding in the UI updates to show only matching products.
Paginating an Observable Array
function PaginatedListViewModel() {
var self = this;
self.allItems = ko.observableArray(generateItems(100));
self.pageSize = ko.observable(10);
self.currentPage = ko.observable(1);
self.totalPages = ko.pureComputed(function() {
return Math.ceil(self.allItems().length / self.pageSize());
});
self.pagedItems = ko.pureComputed(function() {
var start = (self.currentPage() - 1) * self.pageSize();
var end = start + self.pageSize();
return self.allItems().slice(start, end);
});
self.nextPage = function() {
if (self.currentPage() < self.totalPages()) {
self.currentPage(self.currentPage() + 1);
}
};
self.prevPage = function() {
if (self.currentPage() > 1) {
self.currentPage(self.currentPage() - 1);
}
};
self.pageNumbers = ko.pureComputed(function() {
var pages = [];
for (var i = 1; i <= self.totalPages(); i++) {
pages.push(i);
}
return pages;
});
}
Expected output: The pagedItems computed returns only the items for the current page. Pagination controls call nextPage and prevPage to navigate, automatically updating the displayed items.
Removing Items with Click Binding
function ItemViewModel(name) {
var self = this;
self.name = ko.observable(name);
}
function ListViewModel() {
var self = this;
self.items = ko.observableArray([
new ItemViewModel('Item 1'),
new ItemViewModel('Item 2'),
new ItemViewModel('Item 3')
]);
self.removeItem = function(item) {
self.items.remove(item);
};
self.addItem = function() {
self.items.push(new ItemViewModel('Item ' + (self.items().length + 1)));
};
}
// HTML:
// <ul data-bind="foreach: items">
// <li>
// <span data-bind="text: name"></span>
// <button data-bind="click: $parent.removeItem">Remove</button>
// </li>
// </ul>
Expected output: Each item has a Remove button. Clicking it calls removeItem with the item as the parameter, removing it from the array and the DOM.
Common Mistakes
Using native array methods -
items().push(x)modifies the array but does not notify subscribers. Always useitems.push(x).Replacing the entire array when only one item changes - Calling
items(newArray)with thousands of items causes the entire UI to re-render. Use targeted mutations likespliceorreplaceinstead.Not using
$parentinside nested foreach - Inside a foreach, the binding context changes. Use$parentor$rootto access outer ViewModel methods.Modifying items without observables inside - If array items are plain objects, changing a property like
item.name = 'new'does not update the UI. Use observable properties on each item.Forgetting to handle empty state - When the array is empty, the foreach binding renders nothing. Provide a visible message using the
iforvisiblebinding with the array's length.
Practice Questions
- What is the difference between
items.push(x)anditems().push(x)? - How does
subscribewith'arrayChange'differ from a regular subscribe? - Why should you use a computed for filtering instead of directly modifying the observableArray?
- How do you access the parent ViewModel from inside a foreach loop?
- What happens to the DOM when you call
observableArray.sort()?
Challenge: Build a contact list with add, edit, and delete functionality. Each contact has name, phone, and email. Add a search filter computed and pagination controls.
FAQ
Mini Project
Build a music playlist manager with an observableArray of songs (title, artist, duration). Implement add, remove, reorder (move up/move down), shuffle, and a search filter. Display the total duration as a computed.
What's Next
Now that you understand data management, explore Knockout's built-in bindings to connect your ViewModel to rich UI behaviors.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro