Skip to content

Knockout.js Subscriptions — Manual Change Observation and Side Effects

DodaTech Updated 2026-06-28 6 min read

In this tutorial, you will learn about Knockout.js Subscriptions. We cover key concepts, practical examples, and best practices to help you master this topic.

Knockout.js subscriptions are explicit change listeners that fire callbacks when an observable's value changes, enabling side effects, logging, persistence, and integration with external systems.

What You'll Learn

  • Creating subscriptions with subscribe()
  • Disposing subscriptions to prevent memory leaks
  • Using notify and rateLimit with subscriptions
  • Subscribing to array changes with arrayChange
  • Understanding subscription lifecycle

Why It Matters

Not everything belongs in computed observables. Side effects like saving to localStorage, sending analytics events, updating non-Knockout UI, or calling APIs should happen in subscriptions, keeping your pure computed observables side-effect-free.

Real-World Use

A real-time collaborative document editor where every keystroke triggers an auto-save subscription that sends the updated content to the server via Websocket, while a separate subscription broadcasts cursor position to other users.

Subscription Flow

flowchart LR
    A[Observable Changes] --> B[Subscription]
    B --> C[Side Effect]
    C --> D[localStorage Save]
    C --> E[Analytics Event]
    C --> F[API Call]
    C --> G[Non-KO UI Update]
    style B fill:#e6f3ff,stroke:#4a90d9,stroke-width:2px

Basic Subscription

var name = ko.observable('Alice');

// Subscribe to value changes
var subscription = name.subscribe(function(newValue) {
  console.log('Name changed to: ' + newValue);
});

name('Bob');   // Console: Name changed to: Bob
name('Bob');   // No output (same value)
name('Alice'); // Console: Name changed to: Alice

Expected output: The subscription callback fires only when the value actually changes. Setting the same value again produces no notification.

Subscribing with Context (this)

Pass a second argument to set this inside the callback:

function Logger() {
  this.logs = [];

  this.addLog = function(message) {
    this.logs.push(message);
    console.log('Log count: ' + this.logs.length);
  };
}

var logger = new Logger();
var count = ko.observable(0);

// 'this' inside the callback will be 'logger'
count.subscribe(function(newValue) {
  this.addLog('Count changed to ' + newValue);
}, logger);

count(1); // Console: Log count: 1
count(2); // Console: Log count: 2

Disposing Subscriptions

Subscriptions must be disposed when no longer needed to prevent memory leaks:

var data = ko.observable('initial');
var subscription = data.subscribe(function(value) {
  console.log('Data changed: ' + value);
});

// Later, when the component is destroyed:
subscription.dispose();

console.log(subscription.isDisposed()); // Output: true

// After dispose, the callback no longer fires
data('new value'); // No console output

Subscribing to Array Changes

Use 'arrayChange' as the third argument to get detailed change notifications:

var items = ko.observableArray(['a', 'b', 'c']);

items.subscribe(function(changes) {
  changes.forEach(function(change) {
    console.log(
      change.status,      // 'added' or 'deleted'
      change.value,       // The item
      change.index        // Array index
    );
  });
}, null, 'arrayChange');

items.push('d');
// Console: added d 3

items.remove('a');
// Console: deleted a 0

items.splice(1, 0, 'x');
// Console: added x 1

Expected output: The arrayChange subscription provides granular add/remove notifications with the specific item and index, enabling animated transitions and targeted updates.

Subscribing Before a Change (Before Subscription)

Use the 'beforeChange' event to access the previous value:

var value = ko.observable(10);

value.subscribe(function(oldValue) {
  console.log('Value is about to change from: ' + oldValue);
}, null, 'beforeChange');

value.subscribe(function(newValue) {
  console.log('Value changed to: ' + newValue);
});

value(20);
// Console: Value is about to change from: 10
// Console: Value changed to: 20

Subscription with Rate Limiting

Combine subscriptions with rateLimit to debounce side effects:

var searchQuery = ko.observable('').extend({
  rateLimit: { timeout: 300, method: 'notifyWhenChangesStop' }
});

searchQuery.subscribe(function(query) {
  console.log('Searching for: ' + query);
  // Make API call here
});

// Rapid typing only triggers one search
searchQuery('a');
searchQuery('ap');
searchQuery('app');
searchQuery('appl');
searchQuery('apple');
// After 300ms of no typing: Console: Searching for: apple

Multiple Subscriptions on One Observable

var user = ko.observable(null);

// Subscription 1: Update UI
user.subscribe(function(newUser) {
  updateProfileCard(newUser);
});

// Subscription 2: Analytics
user.subscribe(function(newUser) {
  analytics.track('User Selected', { userId: newUser?.id });
});

// Subscription 3: Persistence
user.subscribe(function(newUser) {
  localStorage.setItem('lastViewedUser', JSON.stringify(newUser));
});

Subscription for Computed Observables

Although computed values derive automatically, subscriptions on them are useful for side effects:

var firstName = ko.observable('John');
var lastName = ko.observable('Doe');

var fullName = ko.pureComputed(function() {
  return firstName() + ' ' + lastName();
});

fullName.subscribe(function(newName) {
  document.title = 'Profile: ' + newName;
});

firstName('Jane');
// Document title updates to: Profile: Jane Doe

Subscribable (Custom Event Bus)

Use ko.subscribable for custom pub/sub scenarios:

// Create a custom event bus
var eventBus = new ko.subscribable();

// Component A: Subscribe to events
eventBus.subscribe(function(data) {
  console.log('Received:', data);
});

// Component B: Publish events
function notify(message) {
  eventBus.notifySubscribers(message, 'customEvent');
}

notify('Hello from Component B');
// Console: Received: Hello from Component B

Common Mistakes

  1. Not disposing subscriptions - Subscriptions create references from the observable to the callback. Undisposed subscriptions prevent Garbage Collection of the component that created them.

  2. Creating subscriptions inside computed evaluators - Subscriptions are side effects and should not be created inside computed functions, which are supposed to be pure.

  3. Ignoring the return value of subscribe - subscribe() returns a subscription object. Always capture it so you can dispose it later.

  4. Using 'beforeChange' for UI updates - beforeChange fires with the old value right before the change. Use regular subscribe for most UI update scenarios.

  5. Not using 'arrayChange' for granular updates - Without arrayChange, the subscription receives the entire array every time, making it hard to determine what actually changed.

Practice Questions

  1. What does the subscribe method return?
  2. Why must you dispose subscriptions when a component is destroyed?
  3. How does 'arrayChange' differ from a regular subscription on an observableArray?
  4. What is the purpose of the second argument (context) in subscribe?
  5. When would you use 'beforeChange' instead of the regular 'change' event?

Challenge: Build an auto-saving form where every field change triggers a subscription that saves the form data to localStorage. Include a subscription that shows an "Unsaved changes" warning when the user tries to close the page. Dispose all subscriptions when the form is destroyed.

FAQ

Can a subscription be triggered manually?

Yes, call observable.valueHasMutated() to force notification of all subscribers even if the value did not change.

How many subscriptions can an observable have?

There is no hard limit. Each subscription adds a small memory overhead. Thousands of subscriptions on one observable may affect write performance.

Do subscriptions work with pureComputed observables?

Yes, but pureComputed only tracks subscribers when it is being observed. A subscription on a pureComputed that has no other observers may not behave as expected.

What happens if a subscription callback throws an error?

The error propagates and subsequent subscriptions in the notification chain may not fire. Wrap subscription logic in try/catch blocks to prevent one subscriber from breaking others.

Can I subscribe to an observable before it has any value?

Yes. The subscribe method works on any observable regardless of its current value. The callback fires when the value first changes, not on subscription creation.

Mini Project

Build a real-time dashboard widget that subscribes to multiple data observables. Each subscription updates a specific part of the dashboard, logs changes, and persists the latest state to localStorage. Include a subscription that stops all updates when the dashboard is minimized.

What's Next

For performance-critical observables, learn about pure computed observables that manage their own dependency subscriptions for optimal memory usage.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro