Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(mobx): add support for AbortSignal for reaction, autorun and sync when #3727

Merged
merged 7 commits into from
Jul 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/thirty-tools-confess.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"mobx": minor
---

Added support for `signal` (AbortSignal) in `autorun`, `reaction` and sync `when` options to dispose them
5 changes: 3 additions & 2 deletions docs/reactions.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.<br>
When used with promise version of `when`, the promise rejects with the "WHEN_ABORTED" error.

### `onError`

Expand Down
31 changes: 31 additions & 0 deletions packages/mobx/__tests__/v5/base/autorun.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
42 changes: 42 additions & 0 deletions packages/mobx/__tests__/v5/base/reaction.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = []
Expand Down
12 changes: 12 additions & 0 deletions packages/mobx/__tests__/v5/base/typescript-tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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" })

Expand Down
16 changes: 11 additions & 5 deletions packages/mobx/src/api/autorun.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ import {
isFunction,
isPlainObject,
die,
allowStateChanges
allowStateChanges,
GenericAbortSignal
} from "../internal"

export interface IAutorunOptions {
Expand All @@ -25,6 +26,7 @@ export interface IAutorunOptions {
requiresObservable?: boolean
scheduler?: (callback: () => void) => any
onError?: (error: any) => void
signal?: GenericAbortSignal
}

/**
Expand Down Expand Up @@ -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<T, FireImmediately extends boolean> = IAutorunOptions & {
Expand Down Expand Up @@ -178,8 +182,10 @@ export function reaction<T, FireImmediately extends boolean = false>(
firstTime = false
}

r.schedule_()
return r.getDisposer_()
if(!opts?.signal?.aborted) {
r.schedule_()
}
return r.getDisposer_(opts?.signal)
}

function wrapErrorHandler(errorHandler, baseFn) {
Expand Down
11 changes: 2 additions & 9 deletions packages/mobx/src/api/when.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
15 changes: 10 additions & 5 deletions packages/mobx/src/core/reaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import {
spyReportStart,
startBatch,
trace,
trackDerivedFunction
trackDerivedFunction, GenericAbortSignal
} from "../internal"

/**
Expand Down Expand Up @@ -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() {
Expand Down
1 change: 1 addition & 0 deletions packages/mobx/src/internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
7 changes: 7 additions & 0 deletions packages/mobx/src/types/generic-abort-signal.ts
Original file line number Diff line number Diff line change
@@ -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
}