Skip to content

Commit

Permalink
Add shouldTrackChanges option to @snapshottedView
Browse files Browse the repository at this point in the history
  • Loading branch information
scott-rc committed Jul 10, 2024
1 parent 3dea56c commit c235aad
Show file tree
Hide file tree
Showing 3 changed files with 99 additions and 0 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
90 changes: 90 additions & 0 deletions spec/class-model-snapshotted-views.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>([]);

@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<string>([]);

@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<string>([]);

@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();
});
});
});
7 changes: 7 additions & 0 deletions src/class-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ export interface SnapshottedViewOptions<V, T extends IAnyClassModelType> {
/** 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<T>) => boolean;

/** A function that will be called when the view's reaction throws an error. */
onError?: (error: any) => void;
}
Expand Down Expand Up @@ -303,6 +306,10 @@ export function register<Instance, Klass extends { new (...args: any[]): Instanc
return {
afterCreate() {
for (const view of klass.snapshottedViews) {
if (view.options.shouldTrackChanges && !view.options.shouldTrackChanges(self)) {
continue;
}

reactions.push(
reaction(
() => {
Expand Down

0 comments on commit c235aad

Please sign in to comment.