diff --git a/README.md b/README.md index 722ec7b..f11860a 100644 --- a/README.md +++ b/README.md @@ -510,6 +510,8 @@ const readOnlyExample = TransformExample.createReadOnly(snapshot); readOnlyExample.withoutParams; // => URL { href: "https://example.com" } ``` +Snapshotted views are tracked so that they emit patches when their values change. We track snapshotted views by creating reactions that watch the view's value and compares it to its last value, and bumping a hidden property on the instance when the value changes. If you don't need to track changes to a snapshotted view, you can pass a `shouldTrackChanges` function that returns `false` to the `@snapshottedView` decorator to disable tracking. + ##### Snapshotted view semantics Snapshotted views are a complicated beast, and are best avoided until your performance demands less computation on readonly instances. diff --git a/spec/class-model-snapshotted-views.spec.ts b/spec/class-model-snapshotted-views.spec.ts index 3693607..0c551d4 100644 --- a/spec/class-model-snapshotted-views.spec.ts +++ b/spec/class-model-snapshotted-views.spec.ts @@ -285,4 +285,94 @@ describe("class model snapshotted views", () => { expect(onError).not.toHaveBeenCalled(); }); }); + + describe("shouldTrackChanges", () => { + test("readonly instances don't use the shouldTrackChanges option", () => { + const fn = jest.fn(); + @register + class MyViewExample extends ClassModel({ key: types.identifier, name: types.string }) { + @snapshottedView({ shouldTrackChanges: fn }) + get slug() { + return this.name.toLowerCase().replace(/ /g, "-"); + } + } + + const instance = MyViewExample.createReadOnly({ key: "1", name: "Test" }); + expect(instance.slug).toEqual("test"); + expect(fn).not.toHaveBeenCalled(); + }); + + test("observable instances don't emit a patch when shouldTrackChanges returns false", () => { + const shouldTrackChangesFn = jest.fn(() => false); + const observableArray = observable.array([]); + + @register + class MyViewExample extends ClassModel({ key: types.identifier, name: types.string }) { + @snapshottedView({ shouldTrackChanges: shouldTrackChangesFn }) + get arrayLength() { + return observableArray.length; + } + } + + const instance = MyViewExample.create({ key: "1", name: "Test" }); + expect(shouldTrackChangesFn).toHaveBeenCalled(); + + const onPatchFn = jest.fn(); + onPatch(instance, onPatchFn); + + runInAction(() => { + observableArray.push("a"); + }); + + expect(onPatchFn).not.toHaveBeenCalled(); + }); + + test("observable instances do emit a patch when shouldTrackChanges returns true", () => { + const shouldTrackChangesFn = jest.fn(() => true); + const observableArray = observable.array([]); + + @register + class MyViewExample extends ClassModel({ key: types.identifier, name: types.string }) { + @snapshottedView({ shouldTrackChanges: shouldTrackChangesFn }) + get arrayLength() { + return observableArray.length; + } + } + + const instance = MyViewExample.create({ key: "1", name: "Test" }); + expect(shouldTrackChangesFn).toHaveBeenCalled(); + + const onPatchFn = jest.fn(); + onPatch(instance, onPatchFn); + + runInAction(() => { + observableArray.push("a"); + }); + + expect(onPatchFn).toHaveBeenCalled(); + }); + + test("observable instances do emit a patch when shouldTrackChanges is undefined", () => { + const observableArray = observable.array([]); + + @register + class MyViewExample extends ClassModel({ key: types.identifier, name: types.string }) { + @snapshottedView() + get arrayLength() { + return observableArray.length; + } + } + + const instance = MyViewExample.create({ key: "1", name: "Test" }); + + const onPatchFn = jest.fn(); + onPatch(instance, onPatchFn); + + runInAction(() => { + observableArray.push("a"); + }); + + expect(onPatchFn).toHaveBeenCalled(); + }); + }); }); diff --git a/src/class-model.ts b/src/class-model.ts index f291d5a..88c351b 100644 --- a/src/class-model.ts +++ b/src/class-model.ts @@ -49,6 +49,9 @@ export interface SnapshottedViewOptions { /** A function for converting the view value to a snapshot value */ createSnapshot?: (value: V) => any; + /** A function that determines whether the view should be tracked for changes. */ + shouldTrackChanges?: (node: Instance) => boolean; + /** A function that will be called when the view's reaction throws an error. */ onError?: (error: any) => void; } @@ -303,6 +306,10 @@ export function register {