Skip to content

Commit

Permalink
feat(mobx): add support for AbortSignal for reaction, autorun and…
Browse files Browse the repository at this point in the history
… sync `when` (#3727)
  • Loading branch information
rluvaton committed Jul 18, 2023
1 parent 1a693ef commit bebd5f0
Show file tree
Hide file tree
Showing 10 changed files with 124 additions and 21 deletions.
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
}

0 comments on commit bebd5f0

Please sign in to comment.