Knockout.js Subscriptions — Manual Change Observation and Side Effects
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
notifyandrateLimitwith 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
Not disposing subscriptions - Subscriptions create references from the observable to the callback. Undisposed subscriptions prevent Garbage Collection of the component that created them.
Creating subscriptions inside computed evaluators - Subscriptions are side effects and should not be created inside computed functions, which are supposed to be pure.
Ignoring the return value of subscribe -
subscribe()returns a subscription object. Always capture it so you can dispose it later.Using 'beforeChange' for UI updates -
beforeChangefires with the old value right before the change. Use regular subscribe for most UI update scenarios.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
- What does the
subscribemethod return? - Why must you dispose subscriptions when a component is destroyed?
- How does
'arrayChange'differ from a regular subscription on an observableArray? - What is the purpose of the second argument (context) in
subscribe? - 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
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