From 2ad9eb63e8b41eb6af5e230c04aaed46213e6951 Mon Sep 17 00:00:00 2001 From: Gorbachev Egor <7gorbachevm@gmail.com> Date: Thu, 20 Aug 2026 15:16:39 +0700 Subject: [PATCH 1/3] fix: observe computed dependencies gained in actions --- ...ved-computeds-track-action-dependencies.md | 5 +++++ .../mobx/__tests__/base/become-observed.ts | 21 +++++++++++++++++++ packages/mobx/src/core/computedvalue.ts | 8 ++++++- 3 files changed, 33 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..7a04fbafa 100644 --- a/packages/mobx/__tests__/base/become-observed.ts +++ b/packages/mobx/__tests__/base/become-observed.ts @@ -615,3 +615,24 @@ 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(() => void derived.get()) + + runInAction(() => { + enabled.set(true) + // Recompute the already-observed computed without an outer tracking context + void derived.get() + }) + + expect(events).toEqual(["BO"]) + + disposeAutorun() + expect(events).toEqual(["BO", "BUO"]) +}) diff --git a/packages/mobx/src/core/computedvalue.ts b/packages/mobx/src/core/computedvalue.ts index aa573ac42..8e1cd952f 100644 --- a/packages/mobx/src/core/computedvalue.ts +++ b/packages/mobx/src/core/computedvalue.ts @@ -216,7 +216,13 @@ 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.observers_ && this.observers_.size > 0)) + ) { + // An observed 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()) { From 15e6db9ef34b01a33b4bd8e18d22b8e572997b61 Mon Sep 17 00:00:00 2001 From: Gorbachev Egor <7gorbachevm@gmail.com> Date: Thu, 20 Aug 2026 16:31:36 +0700 Subject: [PATCH 2/3] fix: avoid hooks for temporary computed observers --- .../mobx/__tests__/base/become-observed.ts | 18 ++++++++++++++++++ packages/mobx/src/core/computedvalue.ts | 9 +++------ 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/packages/mobx/__tests__/base/become-observed.ts b/packages/mobx/__tests__/base/become-observed.ts index 7a04fbafa..9d3ad1b9c 100644 --- a/packages/mobx/__tests__/base/become-observed.ts +++ b/packages/mobx/__tests__/base/become-observed.ts @@ -636,3 +636,21 @@ test("onBecomeObserved fires for a dependency gained by an observed computed in 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 8e1cd952f..92cf0d521 100644 --- a/packages/mobx/src/core/computedvalue.ts +++ b/packages/mobx/src/core/computedvalue.ts @@ -216,12 +216,9 @@ export class ComputedValue implements IObservable, IComputedValue, IDeriva reportObserved(this) if (shouldCompute(this)) { let prevTrackingContext = globalState.trackingContext - if ( - !prevTrackingContext && - (this.keepAlive_ || (this.observers_ && this.observers_.size > 0)) - ) { - // An observed computed can be recomputed by an untracked read, for - // example from inside an action. Its dependencies are still + 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 } From 2afa3cc221b9135a545848e89d724585dc979b18 Mon Sep 17 00:00:00 2001 From: Gorbachev Egor <7gorbachevm@gmail.com> Date: Thu, 20 Aug 2026 16:51:57 +0700 Subject: [PATCH 3/3] test: simplify computed lifecycle regression --- packages/mobx/__tests__/base/become-observed.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/mobx/__tests__/base/become-observed.ts b/packages/mobx/__tests__/base/become-observed.ts index 9d3ad1b9c..fdcfdb2c5 100644 --- a/packages/mobx/__tests__/base/become-observed.ts +++ b/packages/mobx/__tests__/base/become-observed.ts @@ -623,12 +623,13 @@ test("onBecomeObserved fires for a dependency gained by an observed computed in onBecomeObserved(resource, () => events.push("BO")) onBecomeUnobserved(resource, () => events.push("BUO")) const derived = computed(() => (enabled.get() ? resource.get() : null)) - const disposeAutorun = autorun(() => void derived.get()) + const disposeAutorun = autorun(() => { + derived.get() + }) runInAction(() => { enabled.set(true) - // Recompute the already-observed computed without an outer tracking context - void derived.get() + derived.get() }) expect(events).toEqual(["BO"])