mobx
Version:
Simple, scalable state management.
173 lines (155 loc) • 4.9 kB
text/typescript
import {
EMPTY_OBJECT,
IEqualsComparer,
IReactionDisposer,
IReactionPublic,
Lambda,
Reaction,
action,
comparer,
getNextId,
isAction,
isFunction,
isPlainObject,
die,
allowStateChanges
} from "../internal"
export interface IAutorunOptions {
delay?: number
name?: string
/**
* Experimental.
* Warns if the view doesn't track observables
*/
requiresObservable?: boolean
scheduler?: (callback: () => void) => any
onError?: (error: any) => void
}
/**
* Creates a named reactive view and keeps it alive, so that the view is always
* updated if one of the dependencies changes, even when the view is not further used by something else.
* @param view The reactive view
* @returns disposer function, which can be used to stop the view from being updated in the future.
*/
export function autorun(
view: (r: IReactionPublic) => any,
opts: IAutorunOptions = EMPTY_OBJECT
): IReactionDisposer {
if (__DEV__) {
if (!isFunction(view)) die("Autorun expects a function as first argument")
if (isAction(view)) die("Autorun does not accept actions since actions are untrackable")
}
const name: string =
opts?.name ?? (__DEV__ ? (view as any).name || "Autorun@" + getNextId() : "Autorun")
const runSync = !opts.scheduler && !opts.delay
let reaction: Reaction
if (runSync) {
// normal autorun
reaction = new Reaction(
name,
function (this: Reaction) {
this.track(reactionRunner)
},
opts.onError,
opts.requiresObservable
)
} else {
const scheduler = createSchedulerFromOptions(opts)
// debounced autorun
let isScheduled = false
reaction = new Reaction(
name,
() => {
if (!isScheduled) {
isScheduled = true
scheduler(() => {
isScheduled = false
if (!reaction.isDisposed_) reaction.track(reactionRunner)
})
}
},
opts.onError,
opts.requiresObservable
)
}
function reactionRunner() {
view(reaction)
}
reaction.schedule_()
return reaction.getDisposer_()
}
export type IReactionOptions = IAutorunOptions & {
fireImmediately?: boolean
equals?: IEqualsComparer<any>
}
const run = (f: Lambda) => f()
function createSchedulerFromOptions(opts: IReactionOptions) {
return opts.scheduler
? opts.scheduler
: opts.delay
? (f: Lambda) => setTimeout(f, opts.delay!)
: run
}
export function reaction<T>(
expression: (r: IReactionPublic) => T,
effect: (arg: T, prev: T, r: IReactionPublic) => void,
opts: IReactionOptions = EMPTY_OBJECT
): IReactionDisposer {
if (__DEV__) {
if (!isFunction(expression) || !isFunction(effect))
die("First and second argument to reaction should be functions")
if (!isPlainObject(opts)) die("Third argument of reactions should be an object")
}
const name = opts.name ?? (__DEV__ ? "Reaction@" + getNextId() : "Reaction")
const effectAction = action(
name,
opts.onError ? wrapErrorHandler(opts.onError, effect) : effect
)
const runSync = !opts.scheduler && !opts.delay
const scheduler = createSchedulerFromOptions(opts)
let firstTime = true
let isScheduled = false
let value: T
let oldValue: T = undefined as any // only an issue with fireImmediately
const equals = (opts as any).compareStructural
? comparer.structural
: opts.equals || comparer.default
const r = new Reaction(
name,
() => {
if (firstTime || runSync) {
reactionRunner()
} else if (!isScheduled) {
isScheduled = true
scheduler!(reactionRunner)
}
},
opts.onError,
opts.requiresObservable
)
function reactionRunner() {
isScheduled = false
if (r.isDisposed_) return
let changed: boolean = false
r.track(() => {
const nextValue = allowStateChanges(false, () => expression(r))
changed = firstTime || !equals(value, nextValue)
oldValue = value
value = nextValue
})
if (firstTime && opts.fireImmediately!) effectAction(value, oldValue, r)
else if (!firstTime && changed) effectAction(value, oldValue, r)
firstTime = false
}
r.schedule_()
return r.getDisposer_()
}
function wrapErrorHandler(errorHandler, baseFn) {
return function () {
try {
return baseFn.apply(this, arguments)
} catch (e) {
errorHandler.call(this, e)
}
}
}