Does a Knockout computed observable recover when a dependency observable is replaced after applyBindings?
0 reputation · 11 Jan 2024, 21:45 UTC
0 reputation · 11 Jan 2024, 21:45 UTC
I have a view model where several computed observables derive values from plain observables, and everything is bound with ko.applyBindings at page load. In some flows the model's shape effectively changes at runtime: an observable that a computed depends on may be replaced with a new observable instance, or a property may be added to the view model after binding has already happened.
My understanding is that a computed re-evaluates only when the dependencies it captured during its last evaluation change. If the underlying observable instance is swapped out, the computed may still be subscribed to the old instance and never see updates from the new one, leaving the UI stale. I am unsure whether this is guaranteed behavior or depends on how the replacement is done.
Assume Knockout 3.5.x. I want a pattern where the computed always reflects the current dependency, without tearing down and re-applying all bindings.
Does replacing an observable instance after applyBindings reliably leave dependent computeds subscribed to the stale instance, or are there documented cases where the binding refreshes on its own?
Is wrapping the dependency in another observable (so the inner instance can be swapped) the intended way to keep computeds live, and does that interact correctly with dispose and binding-context cleanup in 3.x?
Yes — in Knockout 3.5.x, replacing an observable instance after ko.applyBindings reliably leaves dependent computeds subscribed to the old instance. A computed captures the specific observables it read during its last evaluation; swapping the instance out from under it does not trigger re-evaluation, so the UI stays stale. There is no documented case where the binding refreshes on its own in this scenario. The intended fix is to wrap the replaceable dependency in an outer observable and have the computed read through the wrapper.
Knockout's dependency detection is instance-based, not name-based. When a computed evaluates, every observable it touches registers a subscription on that object. If your code later does:
vm.price = ko.observable(42); // new instance assigned to the propertynothing notifies the computed. It is still subscribed to the original price observable, and the binding has no reason to re-read the view-model property. This is guaranteed behavior, not an accident of how the replacement is done — assignment to a plain property is invisible to Knockout regardless of timing.
The same applies to properties added after binding: Knockout bindings resolve their expressions once per evaluation, and a property that did not exist at bind time will not be picked up unless something the binding already tracks changes.
Make the slot itself observable, and let the computed unwrap two levels:
function ViewModel() {
var self = this;
self.current = ko.observable(ko.observable(1)); // outer holds inner
self.display = ko.pureComputed(function () {
var inner = self.current();
return inner ? inner() : undefined;
});
}
// Later: swap the inner observable
vm.current(ko.observable(2)); // display re-evaluates and shows 2When current changes, the computed re-evaluates. During that evaluation Knockout automatically drops the subscription to the old inner observable and subscribes to the new one — dependency tracking handles the resubscription for you. A pureComputed is a good fit here because it also releases its subscriptions entirely when nothing is listening, which keeps binding-context cleanup simple.
You do not need to call dispose on the old inner observable yourself when using this pattern; the computed's re-evaluation unsubscribes from it. If nothing else references the old instance, it becomes garbage like any other object. The cautions worth observing:
ko.observable. Replacing it with a plain property breaks tracking.inner = ko.observable(...)) without writing it through the outer — that is exactly the stale-subscription bug in disguise.null/undefined; the computed will otherwise throw or return undefined into the UI.current()() re-evaluates on inner changes too, so keep the inner observable's update frequency reasonable.A two-minute console check confirms both halves of the claim:
var inner = ko.observable(1);
var vm = { slot: ko.observable(inner) };
var comp = ko.pureComputed(function () { return vm.slot()(); });
console.log(comp()); // 1
// Stale path: replace without going through the wrapper
inner = ko.observable(99);
console.log(comp()); // still 1 — old subscription
// Live path: write through the outer
vm.slot(ko.observable(2));
console.log(comp()); // 2 — resubscribed automaticallyIf your real view model still goes stale after adopting the wrapper, the likely culprit is a binding or computed somewhere that captured the inner observable directly at bind time (for example, binding to current()() in the markup). That is the one diagnostic worth checking before assuming the pattern failed.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.