Skip to content

Knockout.js Observable Arrays — Reactive List Management

DodaTech Updated 2026-06-28 6 min read

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

  1. Using native array methods - items().push(x) modifies the array but does not notify subscribers. Always use items.push(x).

  2. 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 like splice or replace instead.

  3. Not using $parent inside nested foreach - Inside a foreach, the binding context changes. Use $parent or $root to access outer ViewModel methods.

  4. 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.

  5. Forgetting to handle empty state - When the array is empty, the foreach binding renders nothing. Provide a visible message using the if or visible binding with the array's length.

Practice Questions

  1. What is the difference between items.push(x) and items().push(x)?
  2. How does subscribe with 'arrayChange' differ from a regular subscribe?
  3. Why should you use a computed for filtering instead of directly modifying the observableArray?
  4. How do you access the parent ViewModel from inside a foreach loop?
  5. 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

Can an observableArray contain observable objects?

Yes, and this is the recommended pattern for lists where individual items may change. Each item can have observable properties while the array tracks additions and removals.

How do I move an item from one position to another?

Use splice to remove the item from its current position and splice to insert it at the new position. The two operations notify subscribers separately.

Does observableArray support `indexOf`, `filter`, and `map`?

The observableArray itself does not expose these methods, but you can get the underlying array with items() and call any native array method on it. Just remember that mutations through the native array do not notify.

What is the performance impact of large observable arrays?

Knockout handles hundreds of items smoothly. For thousands of items, consider using virtual scrolling (foreach with a paged computed) to render only visible items.

Can I use observableArray with drag-and-drop reordering?

Yes. Use a drag-and-drop library that calls observableArray.splice() to move items on drop. The foreach binding automatically reflects the new order.

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