diff --git a/.changeset/thirty-tools-confess.md b/.changeset/thirty-tools-confess.md
new file mode 100644
index 000000000..44f4fe574
--- /dev/null
+++ b/.changeset/thirty-tools-confess.md
@@ -0,0 +1,5 @@
+---
+"mobx": minor
+---
+
+Added support for `signal` (AbortSignal) in `autorun`, `reaction` and sync `when` options to dispose them
diff --git a/docs/reactions.md b/docs/reactions.md
index c5ef1849f..aaa6d5c7f 100644
--- a/docs/reactions.md
+++ b/docs/reactions.md
@@ -364,9 +364,10 @@ Number of milliseconds that can be used to throttle the effect function. If zero
Set a limited amount of time that `when` will wait for. If the deadline passes, `when` will reject / throw.
-### `signal` _(when)_
+### `signal`
-An AbortSignal object instance; allows you to abort waiting for the reaction via an AbortController. This will also cause the returned promise to reject with an error "WHEN_ABORTED". This option is ignored when using an effect function, and only applies with the promised based version.
+An AbortSignal object instance; can be used as an alternative method for disposal.
+When used with promise version of `when`, the promise rejects with the "WHEN_ABORTED" error.
### `onError`
diff --git a/packages/mobx/__tests__/v5/base/autorun.js b/packages/mobx/__tests__/v5/base/autorun.js
index c84a9b3c5..6cf422207 100644
--- a/packages/mobx/__tests__/v5/base/autorun.js
+++ b/packages/mobx/__tests__/v5/base/autorun.js
@@ -37,6 +37,37 @@ test("autorun can be disposed on first run", function () {
expect(values).toEqual([1])
})
+test("autorun can be disposed using AbortSignal", function () {
+ const a = mobx.observable.box(1)
+ const ac = new AbortController()
+ const values = []
+
+ mobx.autorun(() => {
+ values.push(a.get())
+ }, { signal: ac.signal })
+
+ a.set(2)
+ a.set(3)
+ ac.abort()
+ a.set(4)
+
+ expect(values).toEqual([1, 2, 3])
+})
+
+test("autorun should not run first time when passing already aborted AbortSignal", function () {
+ const a = mobx.observable.box(1)
+ const ac = new AbortController()
+ const values = []
+
+ ac.abort()
+
+ mobx.autorun(() => {
+ values.push(a.get())
+ }, { signal: ac.signal })
+
+ expect(values).toEqual([])
+})
+
test("autorun warns when passed an action", function () {
const action = mobx.action(() => {})
expect.assertions(1)
diff --git a/packages/mobx/__tests__/v5/base/reaction.js b/packages/mobx/__tests__/v5/base/reaction.js
index ebb264826..4ac9e273f 100644
--- a/packages/mobx/__tests__/v5/base/reaction.js
+++ b/packages/mobx/__tests__/v5/base/reaction.js
@@ -260,6 +260,48 @@ test("can dispose reaction on first run", () => {
expect(valuesEffect).toEqual([])
})
+test("can dispose reaction with AbortSignal", () => {
+ const a = mobx.observable.box(1)
+ const ac = new AbortController()
+ const values = []
+
+ reaction(
+ () => a.get(),
+ (newValue, oldValue) => {
+ values.push([newValue, oldValue])
+ },
+ { signal: ac.signal }
+ )
+
+ a.set(2)
+ a.set(3)
+ ac.abort()
+ a.set(4)
+
+ expect(values).toEqual([
+ [2, 1],
+ [3, 2]
+ ])
+})
+
+test("fireImmediately should not be honored when passed already aborted AbortSignal", () => {
+ const a = mobx.observable.box(1)
+ const ac = new AbortController()
+ const values = []
+
+ ac.abort()
+
+ reaction(
+ () => a.get(),
+ (newValue) => {
+ values.push(newValue)
+ },
+ { signal: ac.signal, fireImmediately: true }
+ )
+
+ expect(values).toEqual([])
+})
+
test("#278 do not rerun if expr output doesn't change", () => {
const a = mobx.observable.box(1)
const values = []
diff --git a/packages/mobx/__tests__/v5/base/typescript-tests.ts b/packages/mobx/__tests__/v5/base/typescript-tests.ts
index 1ec5cf6c3..c0a344615 100644
--- a/packages/mobx/__tests__/v5/base/typescript-tests.ts
+++ b/packages/mobx/__tests__/v5/base/typescript-tests.ts
@@ -1820,6 +1820,18 @@ test("promised when can be aborted", async () => {
}
})
+test("sync when can be aborted", async () => {
+ const x = mobx.observable.box(1)
+
+ const ac = new AbortController()
+ mobx.when(() => x.get() === 3, () => {
+ fail("should abort")
+ }, { signal: ac.signal })
+ ac.abort()
+
+ x.set(3);
+})
+
test("it should support asyncAction as decorator (ts)", async () => {
mobx.configure({ enforceActions: "observed" })
diff --git a/packages/mobx/src/api/autorun.ts b/packages/mobx/src/api/autorun.ts
index 1ddc23e4b..ddf96ed3f 100644
--- a/packages/mobx/src/api/autorun.ts
+++ b/packages/mobx/src/api/autorun.ts
@@ -12,7 +12,8 @@ import {
isFunction,
isPlainObject,
die,
- allowStateChanges
+ allowStateChanges,
+ GenericAbortSignal
} from "../internal"
export interface IAutorunOptions {
@@ -25,6 +26,7 @@ export interface IAutorunOptions {
requiresObservable?: boolean
scheduler?: (callback: () => void) => any
onError?: (error: any) => void
+ signal?: GenericAbortSignal
}
/**
@@ -88,8 +90,10 @@ export function autorun(
view(reaction)
}
- reaction.schedule_()
- return reaction.getDisposer_()
+ if(!opts?.signal?.aborted) {
+ reaction.schedule_()
+ }
+ return reaction.getDisposer_(opts?.signal)
}
export type IReactionOptions = IAutorunOptions & {
@@ -178,8 +182,10 @@ export function reaction(
firstTime = false
}
- r.schedule_()
- return r.getDisposer_()
+ if(!opts?.signal?.aborted) {
+ r.schedule_()
+ }
+ return r.getDisposer_(opts?.signal)
}
function wrapErrorHandler(errorHandler, baseFn) {
diff --git a/packages/mobx/src/api/when.ts b/packages/mobx/src/api/when.ts
index 628a33677..6417aebb9 100644
--- a/packages/mobx/src/api/when.ts
+++ b/packages/mobx/src/api/when.ts
@@ -6,17 +6,10 @@ import {
createAction,
getNextId,
die,
- allowStateChanges
+ allowStateChanges,
+ GenericAbortSignal
} from "../internal"
-// https://github.com/mobxjs/mobx/issues/3582
-interface GenericAbortSignal {
- readonly aborted: boolean
- onabort?: ((...args: any) => any) | null
- addEventListener?: (...args: any) => any
- removeEventListener?: (...args: any) => any
-}
-
export interface IWhenOptions {
name?: string
timeout?: number
diff --git a/packages/mobx/src/core/reaction.ts b/packages/mobx/src/core/reaction.ts
index dadfdcb13..7baf5bb41 100644
--- a/packages/mobx/src/core/reaction.ts
+++ b/packages/mobx/src/core/reaction.ts
@@ -18,7 +18,7 @@ import {
spyReportStart,
startBatch,
trace,
- trackDerivedFunction
+ trackDerivedFunction, GenericAbortSignal
} from "../internal"
/**
@@ -195,10 +195,15 @@ export class Reaction implements IDerivation, IReactionPublic {
}
}
- getDisposer_(): IReactionDisposer {
- const r = this.dispose.bind(this) as IReactionDisposer
- r[$mobx] = this
- return r
+ getDisposer_(abortSignal?: GenericAbortSignal): IReactionDisposer {
+ const dispose = (() => {
+ this.dispose()
+ abortSignal?.removeEventListener?.("abort", dispose)
+ }) as IReactionDisposer
+ abortSignal?.addEventListener?.("abort", dispose)
+ dispose[$mobx] = this
+
+ return dispose
}
toString() {
diff --git a/packages/mobx/src/internal.ts b/packages/mobx/src/internal.ts
index c476084fd..1317b234c 100644
--- a/packages/mobx/src/internal.ts
+++ b/packages/mobx/src/internal.ts
@@ -18,6 +18,7 @@ export * from "./types/flowannotation"
export * from "./types/computedannotation"
export * from "./types/observableannotation"
export * from "./types/autoannotation"
+export * from "./types/generic-abort-signal"
export * from "./api/observable"
export * from "./api/computed"
export * from "./core/action"
diff --git a/packages/mobx/src/types/generic-abort-signal.ts b/packages/mobx/src/types/generic-abort-signal.ts
new file mode 100644
index 000000000..3620496ff
--- /dev/null
+++ b/packages/mobx/src/types/generic-abort-signal.ts
@@ -0,0 +1,7 @@
+// https://github.com/mobxjs/mobx/issues/3582
+export interface GenericAbortSignal {
+ readonly aborted: boolean
+ onabort?: ((...args: any) => any) | null
+ addEventListener?: (...args: any) => any
+ removeEventListener?: (...args: any) => any
+}