From 01211a698b64ea94de8e5f276ff8235fcf8ddf96 Mon Sep 17 00:00:00 2001 From: Egor <7gorbachevm@gmail.com> Date: Thu, 20 Aug 2026 16:54:41 +0700 Subject: [PATCH] Fix ComputedValue fails to trigger onBecomeObservable when used in action -> autorun (#4700) * fix: observe computed dependencies gained in actions * fix: avoid hooks for temporary computed observers * test: simplify computed lifecycle regression --- ...ved-computeds-track-action-dependencies.md | 5 +++ .../mobx/__tests__/base/become-observed.ts | 40 +++++++++++++++++++ packages/mobx/src/core/computedvalue.ts | 5 ++- 3 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 .changeset/observed-computeds-track-action-dependencies.md diff --git a/.changeset/observed-computeds-track-action-dependencies.md b/.changeset/observed-computeds-track-action-dependencies.md new file mode 100644 index 000000000..00ddf9e0b --- /dev/null +++ b/.changeset/observed-computeds-track-action-dependencies.md @@ -0,0 +1,5 @@ +--- +"mobx": patch +--- + +Fix `onBecomeObserved` not firing for dependencies gained when an observed computed is recomputed inside an action. diff --git a/packages/mobx/__tests__/base/become-observed.ts b/packages/mobx/__tests__/base/become-observed.ts index 0a4d975eb..fdcfdb2c5 100644 --- a/packages/mobx/__tests__/base/become-observed.ts +++ b/packages/mobx/__tests__/base/become-observed.ts @@ -615,3 +615,43 @@ test("onBecomeObserved cascades through a chain of cached computeds #4547", () = disposeAutorun!() expect(events).toEqual(["BO", "BUO"]) }) + +test("onBecomeObserved fires for a dependency gained by an observed computed in an action #3674", () => { + const events: string[] = [] + const enabled = observable.box(false) + const resource = observable.box(1) + onBecomeObserved(resource, () => events.push("BO")) + onBecomeUnobserved(resource, () => events.push("BUO")) + const derived = computed(() => (enabled.get() ? resource.get() : null)) + const disposeAutorun = autorun(() => { + derived.get() + }) + + runInAction(() => { + enabled.set(true) + derived.get() + }) + + expect(events).toEqual(["BO"]) + + disposeAutorun() + expect(events).toEqual(["BO", "BUO"]) +}) + +test("temporary computed observers do not activate lifecycle hooks", () => { + const events: string[] = [] + const enabled = observable.box(false) + const resource = observable.box(1) + onBecomeObserved(resource, () => events.push("BO")) + onBecomeUnobserved(resource, () => events.push("BUO")) + const inner = computed(() => (enabled.get() ? resource.get() : null)) + const outer = computed(() => inner.get()) + + runInAction(() => { + outer.get() + enabled.set(true) + inner.get() + }) + + expect(events).toEqual([]) +}) diff --git a/packages/mobx/src/core/computedvalue.ts b/packages/mobx/src/core/computedvalue.ts index aa573ac42..92cf0d521 100644 --- a/packages/mobx/src/core/computedvalue.ts +++ b/packages/mobx/src/core/computedvalue.ts @@ -216,7 +216,10 @@ export class ComputedValue implements IObservable, IComputedValue, IDeriva reportObserved(this) if (shouldCompute(this)) { let prevTrackingContext = globalState.trackingContext - if (this.keepAlive_ && !prevTrackingContext) { + if (!prevTrackingContext && (this.keepAlive_ || this.isBeingObserved)) { + // An observed or keep-alive computed can be recomputed by an untracked + // read, for example from inside an action. Its dependencies are still + // transitively observed and must fire their lifecycle hooks globalState.trackingContext = this } if (this.trackAndCompute()) {