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 +}