diff --git a/spec/class-model-snapshotted-views.spec.ts b/spec/class-model-snapshotted-views.spec.ts index 9361601..f59833b 100644 --- a/spec/class-model-snapshotted-views.spec.ts +++ b/spec/class-model-snapshotted-views.spec.ts @@ -2,6 +2,7 @@ import { observable, runInAction } from "mobx"; import { ClassModel, action, snapshottedView, getSnapshot, register, types, onPatch } from "../src"; import { Apple } from "./fixtures/FruitAisle"; import { create } from "./helpers"; +import { getParent } from "mobx-state-tree"; @register class ViewExample extends ClassModel({ key: types.identifier, name: types.string }) { @@ -91,6 +92,39 @@ describe("class model snapshotted views", () => { expect(fn).toMatchSnapshot(); }); + test("an observable instance doesn't re-compute it's view when detached", () => { + const onError = jest.fn(); + + @register + class Child extends ClassModel({}) { + @snapshottedView({ onError }) + get parentsChildLength() { + const parent: Parent = getParent(this, 2); + return parent.children.size; + } + } + + @register + class Parent extends ClassModel({ children: types.map(Child) }) { + @action + addChild(key: string) { + this.children.set(key, {}); + return this.children.get(key); + } + + @action + removeChild(key: string) { + this.children.delete(key); + } + } + + const parent = Parent.create(); + parent.addChild("foo"); + parent.removeChild("foo"); + + expect(onError).not.toHaveBeenCalled(); + }); + test("an observable instance's snapshot includes the snapshotted views epoch", () => { const instance = ViewExample.create({ key: "1", name: "Test" }); expect(getSnapshot(instance)).toEqual({ __snapshottedViewsEpoch: 0, key: "1", name: "Test" }); diff --git a/src/class-model.ts b/src/class-model.ts index 336380d..36c8f02 100644 --- a/src/class-model.ts +++ b/src/class-model.ts @@ -2,7 +2,7 @@ import "reflect-metadata"; import memoize from "lodash.memoize"; import type { Instance, IModelType as MSTIModelType, ModelActions } from "mobx-state-tree"; -import { isRoot, types as mstTypes } from "mobx-state-tree"; +import { isAlive, isRoot, types as mstTypes } from "mobx-state-tree"; import { RegistrationError } from "./errors"; import { InstantiatorBuilder } from "./fast-instantiator"; import { FastGetBuilder } from "./fast-getter"; @@ -10,6 +10,7 @@ import { defaultThrowAction, mstPropsFromQuickProps, propsFromModelPropsDeclarat import { $context, $identifier, + $notAlive, $originalDescriptor, $parent, $quickType, @@ -48,6 +49,9 @@ export interface SnapshottedViewOptions { /** A function for converting the view value to a snapshot value */ createSnapshot?: (value: V) => any; + + /** A function that will be called when the view's reaction throws an error. */ + onError?: (error: any) => void; } /** @internal */ @@ -301,6 +305,9 @@ export function register { + if (!isAlive(self)) { + return $notAlive; + } const value = self[view.property]; if (view.options.createSnapshot) { return view.options.createSnapshot(value); @@ -310,10 +317,12 @@ export function register { - self.__incrementSnapshottedViewsEpoch(); + (result) => { + if (result !== $notAlive) { + self.__incrementSnapshottedViewsEpoch(); + } }, - { equals: comparer.structural }, + { equals: comparer.structural, onError: view.options.onError }, ); } }, diff --git a/src/symbols.ts b/src/symbols.ts index 4546dbf..f004475 100644 --- a/src/symbols.ts +++ b/src/symbols.ts @@ -18,6 +18,9 @@ export const $readOnly = Symbol.for("MQT_readonly"); /** @hidden */ export const $originalDescriptor = Symbol.for("MQT_originalDescriptor"); +/** @hidden */ +export const $notAlive = Symbol.for("MQT_notAlive"); + /** * Set on an type when that type needs to be registered with a decorator before it can be used * @hidden