From 229e66510175b8ad7ea2e4a45601c19597b56dcc Mon Sep 17 00:00:00 2001 From: Bnaya Peretz Date: Sun, 18 Aug 2019 11:48:46 +0300 Subject: [PATCH] feat: Add additional optional dev time checks backport of #2079 --- flow-typed/mobx.js | 6 ++ src/api/autorun.ts | 14 ++++- src/api/configure.ts | 22 ++++++- src/api/when.ts | 5 ++ src/core/action.ts | 5 ++ src/core/derivation.ts | 44 ++++++++++++++ src/core/globalstate.ts | 21 +++++++ src/core/observable.ts | 6 +- src/core/reaction.ts | 3 +- test/base/autorun.js | 70 ++++++++++++++++------ test/base/autorunAsync.js | 89 ++++++++++++++++++--------- test/base/reaction.js | 31 ++++++++++ test/base/strict-mode.js | 123 +++++++++++++++++++++++++++++++++++++- 13 files changed, 386 insertions(+), 53 deletions(-) diff --git a/flow-typed/mobx.js b/flow-typed/mobx.js index 73ed41009..1a5c42b80 100644 --- a/flow-typed/mobx.js +++ b/flow-typed/mobx.js @@ -5,6 +5,8 @@ export type IObservableMapInitialValues = IMapEntries | KeyValueMap< export interface IMobxConfigurationOptions { +enforceActions?: boolean | "strict" | "never" | "always" | "observed"; computedRequiresReaction?: boolean; + reactionRequiresObservable?: boolean; + observableRequiresReaction?: boolean; isolateGlobalState?: boolean; disableErrorBoundaries?: boolean; arrayBuffer?: number; @@ -16,6 +18,10 @@ declare export function configure(options: IMobxConfigurationOptions): void export interface IAutorunOptions { delay?: number; name?: string; + /** + * warn if the derivation has no dependencies after creation/update + */ + requiresObservable?: boolean; scheduler?: (callback: () => void) => any; onError?: (error: any) => void; } diff --git a/src/api/autorun.ts b/src/api/autorun.ts index 997f0d6f8..a950aee67 100644 --- a/src/api/autorun.ts +++ b/src/api/autorun.ts @@ -16,6 +16,11 @@ import { 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 } @@ -49,7 +54,8 @@ export function autorun( function(this: Reaction) { this.track(reactionRunner) }, - opts.onError + opts.onError, + opts.requiresObservable ) } else { const scheduler = createSchedulerFromOptions(opts) @@ -67,7 +73,8 @@ export function autorun( }) } }, - opts.onError + opts.onError, + opts.requiresObservable ) } @@ -138,7 +145,8 @@ export function reaction( scheduler!(reactionRunner) } }, - opts.onError + opts.onError, + opts.requiresObservable ) function reactionRunner() { diff --git a/src/api/configure.ts b/src/api/configure.ts index 2e0c580f4..f62fc370e 100644 --- a/src/api/configure.ts +++ b/src/api/configure.ts @@ -10,6 +10,16 @@ import { export function configure(options: { enforceActions?: boolean | "strict" | "never" | "always" | "observed" computedRequiresReaction?: boolean + /** + * (Experimental) + * Warn if you try to create to derivation / reactive context without accessing any observable. + */ + reactionRequiresObservable?: boolean + /** + * (Experimental) + * Warn if observables are accessed outside a reactive context + */ + observableRequiresReaction?: boolean computedConfigurable?: boolean isolateGlobalState?: boolean disableErrorBoundaries?: boolean @@ -22,7 +32,9 @@ export function configure(options: { computedConfigurable, disableErrorBoundaries, arrayBuffer, - reactionScheduler + reactionScheduler, + reactionRequiresObservable, + observableRequiresReaction } = options if (options.isolateGlobalState === true) { isolateGlobalState() @@ -57,6 +69,14 @@ export function configure(options: { if (computedRequiresReaction !== undefined) { globalState.computedRequiresReaction = !!computedRequiresReaction } + if (reactionRequiresObservable !== undefined) { + globalState.reactionRequiresObservable = !!reactionRequiresObservable + } + if (observableRequiresReaction !== undefined) { + globalState.observableRequiresReaction = !!observableRequiresReaction + + globalState.allowStateReads = !globalState.observableRequiresReaction + } if (computedConfigurable !== undefined) { globalState.computedConfigurable = !!computedConfigurable } diff --git a/src/api/when.ts b/src/api/when.ts index c000a7b79..bbc4e5bb8 100644 --- a/src/api/when.ts +++ b/src/api/when.ts @@ -3,6 +3,11 @@ import { Lambda, fail, getNextId, IReactionDisposer, createAction, autorun } fro export interface IWhenOptions { name?: string timeout?: number + /** + * Experimental. + * Warns if the view doesn't track observables + */ + requiresObservable?: boolean onError?: (error: any) => void } diff --git a/src/core/action.ts b/src/core/action.ts index 1de6e2daa..c798e7699 100644 --- a/src/core/action.ts +++ b/src/core/action.ts @@ -11,6 +11,7 @@ import { untrackedEnd, spyReportEnd } from "../internal" +import { allowStateReadsStart, allowStateReadsEnd } from "./derivation" export interface IAction { isMobxAction: boolean @@ -44,6 +45,7 @@ export function executeAction(actionName: string, fn: Function, scope?: any, arg export interface IActionRunInfo { prevDerivation: IDerivation | null prevAllowStateChanges: boolean + prevAllowStateReads: boolean notifySpy: boolean startTime: number error?: any @@ -69,9 +71,11 @@ export function _startAction(actionName: string, scope: any, args?: IArguments): const prevDerivation = untrackedStart() startBatch() const prevAllowStateChanges = allowStateChangesStart(true) + const prevAllowStateReads = allowStateReadsStart(true) const runInfo = { prevDerivation, prevAllowStateChanges, + prevAllowStateReads, notifySpy, startTime, actionId: globalState.nextActionId++, @@ -91,6 +95,7 @@ export function _endAction(runInfo: IActionRunInfo) { globalState.suppressReactionErrors = true } allowStateChangesEnd(runInfo.prevAllowStateChanges) + allowStateReadsEnd(runInfo.prevAllowStateReads) endBatch() untrackedEnd(runInfo.prevDerivation) if (runInfo.notifySpy) { diff --git a/src/core/derivation.ts b/src/core/derivation.ts index 02cfa35db..a5ff17fd2 100644 --- a/src/core/derivation.ts +++ b/src/core/derivation.ts @@ -55,6 +55,11 @@ export interface IDerivation extends IDepTreeNode { __mapid: string onBecomeStale(): void isTracing: TraceMode + + /** + * warn if the derivation has no dependencies after creation/update + */ + requiresObservable?: boolean } export class CaughtException { @@ -155,12 +160,23 @@ export function checkIfStateModificationsAreAllowed(atom: IAtom) { ) } +export function checkIfStateReadsAreAllowed(observable: IObservable) { + if ( + process.env.NODE_ENV !== "production" && + !globalState.allowStateReads && + globalState.observableRequiresReaction + ) { + console.warn(`[mobx] Observable ${observable.name} being read outside a reactive context`) + } +} + /** * Executes the provided function `f` and tracks which observables are being accessed. * The tracking information is stored on the `derivation` object and the derivation is registered * as observer of any of the accessed observables. */ export function trackDerivedFunction(derivation: IDerivation, f: () => T, context: any) { + const prevAllowStateReads = allowStateReadsStart(true) // pre allocate array allocation + room for variation in deps // array will be trimmed by bindDependencies changeDependenciesStateTo0(derivation) @@ -181,9 +197,27 @@ export function trackDerivedFunction(derivation: IDerivation, f: () => T, con } globalState.trackingDerivation = prevTracking bindDependencies(derivation) + + if (derivation.observing.length === 0) { + warnAboutDerivationWithoutDependencies(derivation) + } + + allowStateReadsEnd(prevAllowStateReads) + return result } +function warnAboutDerivationWithoutDependencies(derivation: IDerivation) { + if (process.env.NODE_ENV === "production") return + if (globalState.reactionRequiresObservable || derivation.requiresObservable) { + console.warn( + `[mobx] Derivation ${ + derivation.name + } is created/updated without reading any observable value` + ) + } +} + /** * diffs newObserving with observing. * update observing to be newObserving with unique observables @@ -276,6 +310,16 @@ export function untrackedEnd(prev: IDerivation | null) { globalState.trackingDerivation = prev } +export function allowStateReadsStart(allowStateReads: boolean) { + const prev = globalState.allowStateReads + globalState.allowStateReads = allowStateReads + return prev +} + +export function allowStateReadsEnd(prev: boolean) { + globalState.allowStateReads = prev +} + /** * needed to keep `lowestObserverState` correct. when changing from (2 or 1) to 0 * diff --git a/src/core/globalstate.ts b/src/core/globalstate.ts index 8a6f185e6..1b6cdad25 100644 --- a/src/core/globalstate.ts +++ b/src/core/globalstate.ts @@ -8,6 +8,9 @@ const persistentKeys: (keyof MobXGlobals)[] = [ "spyListeners", "enforceActions", "computedRequiresReaction", + "reactionRequiresObservable", + "observableRequiresReaction", + "allowStateReads", "disableErrorBoundaries", "runId", "UNCHANGED" @@ -81,6 +84,12 @@ export class MobXGlobals { */ allowStateChanges = true + /** + * Is it allowed to read observables at this point? + * Used to hold the state needed for `observableRequiresReaction` + */ + allowStateReads = true + /** * If strict mode is enabled, state changes are by default not allowed */ @@ -101,6 +110,18 @@ export class MobXGlobals { */ computedRequiresReaction = false + /** + * (Experimental) + * Warn if you try to create to derivation / reactive context without accessing any observable. + */ + reactionRequiresObservable = false + + /** + * (Experimental) + * Warn if observables are accessed outside a reactive context + */ + observableRequiresReaction = false + /** * Allows overwriting of computed properties, useful in tests but not prod as it can cause * memory leaks. See https://github.com/mobxjs/mobx/issues/1867 diff --git a/src/core/observable.ts b/src/core/observable.ts index 1849e5af1..9e0172cee 100644 --- a/src/core/observable.ts +++ b/src/core/observable.ts @@ -6,7 +6,8 @@ import { runReactions, ComputedValue, getDependencyTree, - IDependencyTree + IDependencyTree, + checkIfStateReadsAreAllowed } from "../internal" export interface IDepTreeNode { @@ -153,6 +154,8 @@ export function endBatch() { } export function reportObserved(observable: IObservable): boolean { + checkIfStateReadsAreAllowed(observable) + const derivation = globalState.trackingDerivation if (derivation !== null) { /** @@ -172,6 +175,7 @@ export function reportObserved(observable: IObservable): boolean { } else if (observable.observers.length === 0 && globalState.inBatch > 0) { queueForUnobservation(observable) } + return false } diff --git a/src/core/reaction.ts b/src/core/reaction.ts index bccc60c8c..fb3b07f68 100644 --- a/src/core/reaction.ts +++ b/src/core/reaction.ts @@ -66,7 +66,8 @@ export class Reaction implements IDerivation, IReactionPublic { constructor( public name: string = "Reaction@" + getNextId(), private onInvalidate: () => void, - private errorHandler?: (error: any, derivation: IDerivation) => void + private errorHandler?: (error: any, derivation: IDerivation) => void, + public requiresObservable = false ) {} onBecomeStale() { diff --git a/test/base/autorun.js b/test/base/autorun.js index 8b18bd7e0..65216cd2b 100644 --- a/test/base/autorun.js +++ b/test/base/autorun.js @@ -1,10 +1,14 @@ -const m = require("../../src/mobx.ts") +/** + * @type {typeof import("./../../src/mobx")} + */ +const mobx = require("../../src/mobx.ts") +const utils = require("../utils/test-utils") test("autorun passes Reaction as an argument to view function", function() { - const a = m.observable.box(1) + const a = mobx.observable.box(1) const values = [] - m.autorun(r => { + mobx.autorun(r => { expect(typeof r.dispose).toBe("function") if (a.get() === "pleaseDispose") r.dispose() values.push(a.get()) @@ -20,10 +24,10 @@ test("autorun passes Reaction as an argument to view function", function() { }) test("autorun can be disposed on first run", function() { - const a = m.observable.box(1) + const a = mobx.observable.box(1) const values = [] - m.autorun(r => { + mobx.autorun(r => { r.dispose() values.push(a.get()) }) @@ -34,9 +38,9 @@ test("autorun can be disposed on first run", function() { }) test("autorun warns when passed an action", function() { - const action = m.action(() => {}) + const action = mobx.action(() => {}) expect.assertions(1) - expect(() => m.autorun(action)).toThrowError(/Autorun does not accept actions/) + expect(() => mobx.autorun(action)).toThrowError(/Autorun does not accept actions/) }) test("autorun batches automatically", function() { @@ -44,7 +48,7 @@ test("autorun batches automatically", function() { let a1runs = 0 let a2runs = 0 - const x = m.observable({ + const x = mobx.observable({ a: 1, b: 1, c: 1, @@ -54,12 +58,12 @@ test("autorun batches automatically", function() { } }) - const d1 = m.autorun(() => { + const d1 = mobx.autorun(() => { a1runs++ x.d // read }) - const d2 = m.autorun(() => { + const d2 = mobx.autorun(() => { a2runs++ x.b = x.a x.c = x.a @@ -80,12 +84,12 @@ test("autorun batches automatically", function() { }) test("autorun tracks invalidation of unbound dependencies", function() { - const a = m.observable.box(0) - const b = m.observable.box(0) - const c = m.computed(() => a.get() + b.get()) + const a = mobx.observable.box(0) + const b = mobx.observable.box(0) + const c = mobx.computed(() => a.get() + b.get()) const values = [] - m.autorun(() => { + mobx.autorun(() => { values.push(c.get()) b.set(100) }) @@ -95,21 +99,49 @@ test("autorun tracks invalidation of unbound dependencies", function() { }) test("when effect is an action", function(done) { - const a = m.observable.box(0) + const a = mobx.observable.box(0) - m.configure({ enforceActions: "observed" }) - m.when( + mobx.configure({ enforceActions: "observed" }) + mobx.when( () => a.get() === 1, () => { a.set(2) - m.configure({ enforceActions: "never" }) + mobx.configure({ enforceActions: "never" }) done() }, { timeout: 1 } ) - m.runInAction(() => { + mobx.runInAction(() => { a.set(1) }) }) + +describe("autorun opts requiresObservable", () => { + test("warn when no observable", () => { + utils.consoleWarn(() => { + const disposer = mobx.autorun(() => 2, { + requiresObservable: true + }) + + disposer() + }, /is created\/updated without reading any observable value/) + }) + + test("Don't warn when observable", () => { + const obsr = mobx.observable({ + x: 1 + }) + + const messages = utils.supressConsole(() => { + const disposer = mobx.autorun(() => obsr.x, { + requiresObservable: true + }) + + disposer() + }) + + expect(messages.length).toBe(0) + }) +}) diff --git a/test/base/autorunAsync.js b/test/base/autorunAsync.js index f212b3e15..e7f08fd45 100644 --- a/test/base/autorunAsync.js +++ b/test/base/autorunAsync.js @@ -1,4 +1,11 @@ -const m = require("../../src/mobx.ts") +/** + * @type {typeof import("../../src/mobx")} + */ +const mobx = require("../../src/mobx.ts") + +const utils = require("../utils/test-utils") + +const { $mobx } = mobx test("autorun 1", function(done) { let _fired = 0 @@ -14,24 +21,24 @@ test("autorun 1", function(done) { _cCalcs = 0 } - const a = m.observable.box(2) - const b = m.observable.box(3) - const c = m.computed(function() { + const a = mobx.observable.box(2) + const b = mobx.observable.box(3) + const c = mobx.computed(function() { _cCalcs++ return a.get() * b.get() }) - const d = m.observable.box(1) + const d = mobx.observable.box(1) const autorun = function() { _fired++ _result = d.get() > 0 ? a.get() * c.get() : d.get() } - let disp = m.autorun(autorun, { delay: 20 }) + let disp = mobx.autorun(autorun, { delay: 20 }) check(0, 0, null) disp() to(function() { check(0, 0, null) - disp = m.autorun(autorun, { delay: 20 }) + disp = mobx.autorun(autorun, { delay: 20 }) to(function() { check(1, 1, 12) @@ -80,12 +87,12 @@ test("autorun 1", function(done) { test("autorun should not result in loop", function(done) { let i = 0 - const a = m.observable({ + const a = mobx.observable({ x: i }) let autoRunsCalled = 0 - const d = m.autorun( + const d = mobx.autorun( function() { autoRunsCalled++ a.x = ++i @@ -106,11 +113,11 @@ test("autorun should not result in loop", function(done) { }) test("autorunAsync passes Reaction as an argument to view function", function(done) { - const a = m.observable.box(1) + const a = mobx.observable.box(1) let autoRunsCalled = 0 - m.autorun( + mobx.autorun( r => { expect(typeof r.dispose).toBe("function") autoRunsCalled++ @@ -131,7 +138,7 @@ test("autorunAsync passes Reaction as an argument to view function", function(do }) test("autorunAsync accepts a scheduling function", function(done) { - const a = m.observable({ + const a = mobx.observable({ x: 0, y: 1 }) @@ -139,7 +146,7 @@ test("autorunAsync accepts a scheduling function", function(done) { let autoRunsCalled = 0 let schedulingsCalled = 0 - m.autorun( + mobx.autorun( function() { autoRunsCalled++ expect(a.y).toBe(a.x + 1) @@ -170,7 +177,7 @@ test("autorunAsync accepts a scheduling function", function(done) { }) test("reaction accepts a scheduling function", function(done) { - const a = m.observable({ + const a = mobx.observable({ x: 0, y: 1 }) @@ -181,7 +188,7 @@ test("reaction accepts a scheduling function", function(done) { const values = [] - m.reaction( + mobx.reaction( () => { exprCalled++ return a.x @@ -218,26 +225,26 @@ test("reaction accepts a scheduling function", function(done) { }) test("autorunAsync warns when passed an action", function() { - const action = m.action(() => {}) + const action = mobx.action(() => {}) expect.assertions(1) - expect(() => m.autorun(action)).toThrowError(/Autorun does not accept actions/) + expect(() => mobx.autorun(action)).toThrowError(/Autorun does not accept actions/) }) test("whenWithTimeout should operate normally", done => { - const a = m.observable.box(1) + const a = mobx.observable.box(1) - m.when(() => a.get() === 2, () => done(), { + mobx.when(() => a.get() === 2, () => done(), { timeout: 500, onError: () => done.fail("error triggered") }) - setTimeout(m.action(() => a.set(2)), 200) + setTimeout(mobx.action(() => a.set(2)), 200) }) test("whenWithTimeout should timeout", done => { - const a = m.observable.box(1) + const a = mobx.observable.box(1) - m.when(() => a.get() === 2, () => done.fail("should have timed out"), { + mobx.when(() => a.get() === 2, () => done.fail("should have timed out"), { timeout: 500, onError: e => { expect("" + e).toMatch(/WHEN_TIMEOUT/) @@ -245,18 +252,18 @@ test("whenWithTimeout should timeout", done => { } }) - setTimeout(m.action(() => a.set(2)), 1000) + setTimeout(mobx.action(() => a.set(2)), 1000) }) test("whenWithTimeout should dispose", done => { - const a = m.observable.box(1) + const a = mobx.observable.box(1) - const d1 = m.when(() => a.get() === 2, () => done.fail("1 should not finsih"), { + const d1 = mobx.when(() => a.get() === 2, () => done.fail("1 should not finsih"), { timeout: 100, onError: () => done.fail("1 should not timeout") }) - const d2 = m.when(() => a.get() === 2, () => t.fail("2 should not finsih"), { + const d2 = mobx.when(() => a.get() === 2, () => t.fail("2 should not finsih"), { timeout: 200, onError: () => done.fail("2 should not timeout") }) @@ -265,10 +272,38 @@ test("whenWithTimeout should dispose", done => { d2() setTimeout( - m.action(() => { + mobx.action(() => { a.set(2) done() }), 150 ) }) + +describe("when opts requiresObservable", () => { + test("warn when no observable", () => { + utils.consoleWarn(() => { + const disposer = mobx.when(() => 2, { + requiresObservable: true + }) + + disposer.cancel() + }, /is created\/updated without reading any observable value/) + }) + + test("Don't warn when observable", () => { + const obsr = mobx.observable({ + x: 1 + }) + + const messages = utils.supressConsole(() => { + const disposer = mobx.when(() => obsr.x, { + requiresObservable: true + }) + + disposer.cancel() + }) + + expect(messages.length).toBe(0) + }) +}) diff --git a/test/base/reaction.js b/test/base/reaction.js index dc29372a9..cdced6db9 100644 --- a/test/base/reaction.js +++ b/test/base/reaction.js @@ -1,3 +1,6 @@ +/** + * @type {typeof import("./../../src/mobx")} + */ const mobx = require("../../src/mobx.ts") const reaction = mobx.reaction const utils = require("../utils/test-utils") @@ -613,3 +616,31 @@ test("Introduce custom onError for - when - 2", () => { expect(globalHandlerCalled).toBe(false) d() }) + +describe("reaction opts requiresObservable", () => { + test("warn when no observable", () => { + utils.consoleWarn(() => { + const disposer = mobx.reaction(() => 2, () => 1, { + requiresObservable: true + }) + + disposer() + }, /is created\/updated without reading any observable value/) + }) + + test("Don't warn when observable", () => { + const obsr = mobx.observable({ + x: 1 + }) + + const messages = utils.supressConsole(() => { + const disposer = mobx.reaction(() => obsr.x, () => 1, { + requiresObservable: true + }) + + disposer() + }) + + expect(messages.length).toBe(0) + }) +}) diff --git a/test/base/strict-mode.js b/test/base/strict-mode.js index 58bb01dbf..09afbf188 100644 --- a/test/base/strict-mode.js +++ b/test/base/strict-mode.js @@ -1,3 +1,6 @@ +/** + * @type {typeof import("../../src/mobx")} + */ const mobx = require("../../src/mobx.ts") const utils = require("../utils/test-utils") @@ -224,7 +227,7 @@ test("enforceActions 'strict' should not throw exception while observable array } }) -test("warn on unsafe reads", function() { +test("warn on unsafe reads of computed", function() { try { mobx.configure({ computedRequiresReaction: true }) const x = mobx.observable({ @@ -241,6 +244,124 @@ test("warn on unsafe reads", function() { } }) +describe("observableRequiresReaction", function() { + test("warn on unsafe reads of observable", function() { + try { + mobx.configure({ observableRequiresReaction: true }) + const x = mobx.observable({ + y: 3 + }) + utils.consoleWarn(() => { + x.y + }, /being read outside a reactive context/) + } finally { + mobx.configure({ observableRequiresReaction: false }) + } + }) + + test("warn on unsafe reads of observable also when there are other subscriptions", function() { + try { + mobx.configure({ observableRequiresReaction: true }) + const x = mobx.observable({ + y: 3 + }) + + const dispose = mobx.autorun(() => x.y) + + utils.consoleWarn(() => { + x.y + }, /being read outside a reactive context/) + + dispose() + } finally { + mobx.configure({ observableRequiresReaction: false }) + } + }) + + test("warn on unsafe reads of observable array", function() { + try { + mobx.configure({ observableRequiresReaction: true }) + const x = mobx.observable({ + arr: [1, 2, 3] + }) + utils.consoleWarn(() => { + x.arr[1] + }, /being read outside a reactive context/) + } finally { + mobx.configure({ observableRequiresReaction: false }) + } + }) + test("don't warn on reads inside a computed", function() { + try { + mobx.configure({ observableRequiresReaction: true }) + const x = mobx.observable({ + y: 1 + }) + + const fooComputed = mobx.computed(() => x.y + 1) + + const messages = utils.supressConsole(() => { + const dispose = mobx.autorun(() => fooComputed.get()) + dispose() + }) + + expect(messages.length).toBe(0) + } finally { + mobx.configure({ observableRequiresReaction: false }) + } + }) + + test("don't warn on reads inside an action", function() { + try { + mobx.configure({ observableRequiresReaction: true }) + const x = mobx.observable({ + y: 1 + }) + + const fooAction = mobx.action(() => x.y) + + const messages = utils.supressConsole(() => { + fooAction() + }) + + expect(messages.length).toBe(0) + } finally { + mobx.configure({ observableRequiresReaction: false }) + } + }) + + test("warn on reads inside a transaction", function() { + try { + mobx.configure({ observableRequiresReaction: true }) + const x = mobx.observable({ + y: 1 + }) + + utils.consoleWarn(() => { + mobx.transaction(() => x.y) + }, /being read outside a reactive context/) + } finally { + mobx.configure({ observableRequiresReaction: false }) + } + }) +}) + +describe("reactionRequiresObservable", function() { + test("warn on reaction creation without dependencies", function() { + try { + mobx.configure({ reactionRequiresObservable: true }) + + utils.consoleWarn(() => { + const dispose = mobx.reaction(() => "plain value", newValue => newValue) + + dispose() + }, /is created\/updated without reading any observable value/) + } finally { + mobx.configure({ reactionRequiresObservable: false }) + } + }) +}) + test("#1869", function() { const x = mobx.observable.box(3) mobx.configure({ enforceActions: "always", isolateGlobalState: true })