Knockout computed() versus subscribe(): derived state without update loops
Use Knockout computed() for derived UI state and .subscribe() for side effects, then guard subscriptions to avoid infinite update loops.
09 Nov 2026, 14:40 UTC

The problem: a total that keeps calling the API
You are building an invoice editor in Knockout. Each line item has a quantity and a price. A read-only total should update whenever any item changes, and that same total should be sent to a pricing API so the server can validate discounts.
You bind the total to an observable, write a computed() to sum the line items, then add a .subscribe() that posts to the API whenever the total changes. Instead, the API fires on every keystroke, or the API response writes back to the total and triggers another subscription, creating an infinite loop.
The problem is not Knockout’s reactivity. It is mixing two different jobs: deriving a value for the UI, and performing an imperative side effect.
Use computed() for derived, read-only UI state
An observable is a function that wraps a value and notifies subscribers whenever that value changes. Reading an observable inside a computed creates a dependency, so Knockout automatically recalculates the computed whenever any of its inputs change.
A computed is the right tool for totals, visibility flags, filtered lists, and anything else that is purely derived from other observables. It should not call APIs, modify the DOM directly, or write to other observables.
function LineItem(quantity, price) {
this.quantity = ko.observable(quantity);
this.price = ko.observable(price);
}
function InvoiceViewModel() {
this.items = ko.observableArray([
new LineItem(2, 10),
new LineItem(1, 25)
]);
this.total = ko.computed(function () {
let sum = 0;
this.items().forEach(function (item) {
sum += item.quantity() * item.price();
});
return sum;
}, this);
}
ko.applyBindings(new InvoiceViewModel());
Because total is a computed, it updates automatically when any item’s quantity or price changes. The UI binding reflects the new value immediately.
Use .subscribe() for side effects outside Knockout
.subscribe(callback) runs imperatively whenever the observable changes. Use it for side effects such as API calls, logging, analytics, or syncing with a non-Knockout library. Do not use it to derive values that belong in the UI; that is what computed is for.
The danger is re-entrancy. If a subscription writes to an observable that a computed reads, the computed recalculates, which fires the subscription again, and so on.
Worked example: validate the total with an API
This example posts the total to an API after the user stops changing values for 300 milliseconds. It does not write back to total, which prevents the loop.
function InvoiceViewModel() {
this.items = ko.observableArray([
new LineItem(2, 10),
new LineItem(1, 25)
]);
this.total = ko.computed(function () {
let sum = 0;
this.items().forEach(function (item) {
sum += item.quantity() * item.price();
});
return sum;
}, this);
let pending = null;
this.total.subscribe(function (newTotal) {
if (pending) {
clearTimeout(pending);
}
pending = setTimeout(function () {
pending = null;
// Side effect: sync with external API.
// Do not assign the response back to this.total here.
fetch('/api/validate-price', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ total: newTotal })
});
}, 300);
});
}
Run this in a browser page where Knockout is loaded. The fetch call happens once after the user pauses, not on every intermediate change.
How to verify reactivity without guessing
Open Chrome DevTools and run these commands in the console while the page is running:
ko.dataFor(document.getElementById('total-display'))— inspects the view model bound to a specific DOM node. Replace'total-display'with the actual ID of the element showing the total.ko.toJSON(viewModel)— serializes the current view model so you can confirm the total matches the items.- Add a temporary subscription such as
viewModel.total.subscribe(function (v) { console.log('total changed', v); })to confirm updates fire only when expected. Remove it before committing.
Expected check: change a quantity in the UI, confirm the total text updates, then confirm the network tab shows a single API call after the debounce window rather than one per keystroke.
Trade-offs and limitations
- Computed evaluators run synchronously on every dependency change. Expensive logic inside a computed can block the UI.
- Subscriptions also run synchronously. Long-running work inside a subscription should be deferred with
setTimeout,fetch, or a task queue. - Knockout does not provide built-in debouncing or throttling; you must add it manually as shown above.
- Deeply nested observable hierarchies can degrade performance in older browsers. Profile with Chrome DevTools if you notice sluggish updates.
Decision checklist
- Is the value used only in the UI and derived from other observables? Use
ko.computed(). - Does the value need to trigger an API call, log, or external sync? Use
.subscribe(). - Will the side effect write back to an observable the computed depends on? If yes, store the external result in a separate observable or pause bindings during the sync.
- Does the side effect fire too often? Add debouncing or a change guard inside the subscription.
Knockout’s reactivity works best when you separate derivation from side effects. Keep computeds pure, keep subscriptions focused, and inspect live state with ko.dataFor() and ko.toJSON() when the UI stops behaving as expected.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.