Knockout Computed Observables: Dependency Tracking, Write Patterns, and Disposal
Knockout's ko.computed tracks observable dependencies during evaluation and re-runs when they change. This guide covers read/write patterns, pureComputed, disposal, and common mistakes like infinite loops and async dependency loss.
06 Sept 2025, 23:34 UTC

The problem: derived state that stays in sync
Knockout applications often need values that derive from other observables — a full name from first and last, a filtered list from a search term, or a total from line items. Manually subscribing to each source and updating a target creates boilerplate and subtle bugs when sources change order or disappear. ko.computed solves this by automatically tracking which observables you read during evaluation and re-running only when those specific dependencies change.
How dependency tracking works
When a computed's evaluator function runs, Knockout records every observable or observableArray accessed. On subsequent changes, only those recorded dependencies trigger re-evaluation. If a branch isn't executed — for example, an observable read inside an if that evaluates false — it isn't subscribed. This means conditional dependencies are dynamic: they attach and detach as the evaluator's logic changes.
Re-evaluation is synchronous and immediate. If multiple dependencies change in a single tick, the computed runs once per change unless you batch updates with ko.computed.deferEvaluation or use ko.tasks (Knockout 3.5+).
Worked example: read/write computed for a price calculator
The following pattern shows a computed that formats a numeric price for display and parses user input back to a number. Run this in a browser console with Knockout 3.5+ loaded.
function OrderLine() {
this.unitPrice = ko.observable(10.00);
this.quantity = ko.observable(2);
this.formattedTotal = ko.computed({
read: function() {
const total = this.unitPrice() * this.quantity();
return total.toFixed(2);
},
write: function(value) {
const parsed = parseFloat(value);
if (!isNaN(parsed)) {
this.unitPrice(parsed / this.quantity());
}
},
owner: this
});
}
const line = new OrderLine();
console.log(line.formattedTotal()); // 20.00
line.quantity(3);
console.log(line.formattedTotal()); // 30.00 — re-evaluated because quantity changed
line.formattedTotal('45.00');
console.log(line.unitPrice()); // 15 — write function ran, unitPrice updated
Where to run: Browser developer console on any page that loads Knockout (or a local HTML file with <script src='knockout-3.5.1.js'></script>). Permissions: None beyond JS execution. Expected checks: Verify the log output matches the comments. Change unitPrice or quantity and confirm formattedTotal updates. Assign to formattedTotal and confirm unitPrice changes. Risks: The write function divides by quantity(); if quantity becomes zero, you'll get Infinity. Guard against that in production code.
Pure computeds for side-effect-free derivations
When an evaluator has no side effects and depends only on observables, use ko.pureComputed. Knockout can then suspend evaluation when no subscribers exist (no UI bindings, no other computeds reading it) and automatically dispose subscriptions when the last subscriber disappears. This reduces memory pressure in long-lived apps with transient UI sections.
this.fullName = ko.pureComputed(function() {
return this.firstName() + ' ' + this.lastName();
}, this);
If fullName is bound in a template that gets removed, its subscriptions to firstName and lastName are released automatically. A regular ko.computed would keep them alive until explicit disposal.
Disposal: when and how to call dispose()
Call computed.dispose() when:
- A computed is created inside a component or custom binding that gets torn down (for example,
ko.components.registerwith adisposecallback). - A computed outlives its logical owner — for example, a computed on a view model that stays in memory after its DOM is removed.
- You create computeds in a loop or factory and need to clean up the old ones.
After dispose(), the evaluator will not run again, even if dependencies change. The computed's value remains accessible but becomes stale.
const temp = ko.computed(() => expensiveCalculation(source()));
// ... later, when temp is no longer needed:
temp.dispose();
Verification: Add console.log('eval') inside the evaluator. Call dispose(), then change source(). The log should not appear.
Common mistakes that break reactivity or leak memory
Assigning to a read-only computed
A computed created with a single function argument is read-only. Writing to it throws in strict mode or silently fails otherwise. Always provide a write function in the options object for two-way binding.
// Wrong — read-only
this.bad = ko.computed(() => this.a() + this.b());
this.bad(5);
// Correct — read/write
this.good = ko.computed({
read: () => this.a() + this.b(),
write: (val) => { this.a(val - this.b()); },
owner: this
});
Mutating observables inside the read evaluator
Changing an observable (or observableArray) during evaluation triggers immediate re-evaluation, causing an infinite loop. Move side effects to subscriptions or the write function instead.
// Infinite loop
this.loop = ko.computed(() => {
const v = this.source();
this.other(v * 2);
return v;
});
// Fixed: use a subscription for side effects
this.source.subscribe((v) => this.other(v * 2));
this.clean = ko.computed(() => this.source());
Reading observables asynchronously
Dependencies are only tracked during synchronous execution of the evaluator. Reading an observable inside setTimeout, Promise.then, or an event handler does not create a subscription.
// Broken: async read doesn't track
this.asyncBroken = ko.computed(() => {
setTimeout(() => this.source(), 0);
return 'static';
});
// Workaround: read synchronously, use value later
this.asyncFixed = ko.computed(() => {
const snap = this.source();
setTimeout(() => console.log(snap), 0);
return snap;
});
Limitations and practical checks
- Synchronous cascade: Rapid dependency changes (for example, typing in an input bound to an observable) fire the computed on every keystroke. Debounce at the source with
ko.observable(...).extend({ rateLimit: 50 })if needed. - A computed re-evaluates even if the new value equals the old one. Downstream bindings may update unnecessarily. Use
ko.computed(...).extend({ notify: 'always' })only when you need forced notifications; otherwise, consider a custom equality check in the write function or a subscriber. - Two computeds reading each other cause stack overflow. Knockout does not detect this; design data flow as a DAG.
Practical verification checklist:
- Open the console, add
console.login each evaluator. - Trigger each source observable change; confirm the computed logs once per logical change.
- Call
dispose()on any computed tied to removed UI; change sources again; confirm no logs. - Test the write path: assign to the computed, verify the correct source observables update.
When to choose computed vs. manual subscription
Use ko.computed (or ko.pureComputed) when the derived value is a pure function of observables and you want automatic dependency management. Use manual subscribe when you need side effects (AJAX, logging, imperative DOM updates) or when the derivation logic is too complex for a single evaluator and you want explicit control over when it runs.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.