Skip to content

Commit 688ba37

Browse files
authored
Merge pull request #107 from gadget-inc/sc/should-track-changes
2 parents 3dea56c + e96e7a5 commit 688ba37

File tree

3 files changed

+186
-18
lines changed

3 files changed

+186
-18
lines changed

README.md

+2
Original file line numberDiff line numberDiff line change
@@ -510,6 +510,8 @@ const readOnlyExample = TransformExample.createReadOnly(snapshot);
510510
readOnlyExample.withoutParams; // => URL { href: "https://example.com" }
511511
```
512512

513+
Snapshotted views emit patches when their values change. If you don't want snapshotted views to emit a patch when they change, you can pass a `shouldEmitPatchOnChange` function that returns `false` to the `@snapshottedView`, or you can pass `false` to `setDefaultShouldEmitPatchOnChange` to disable patch emission for all snapshotted views.
514+
513515
##### Snapshotted view semantics
514516

515517
Snapshotted views are a complicated beast, and are best avoided until your performance demands less computation on readonly instances.

spec/class-model-snapshotted-views.spec.ts

+146
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { ClassModel, action, snapshottedView, getSnapshot, register, types, onPa
33
import { Apple } from "./fixtures/FruitAisle";
44
import { create } from "./helpers";
55
import { getParent } from "mobx-state-tree";
6+
import { setDefaultShouldEmitPatchOnChange } from "../src/class-model";
67

78
@register
89
class ViewExample extends ClassModel({ key: types.identifier, name: types.string }) {
@@ -285,4 +286,149 @@ describe("class model snapshotted views", () => {
285286
expect(onError).not.toHaveBeenCalled();
286287
});
287288
});
289+
290+
describe("shouldEmitPatchOnChange", () => {
291+
afterEach(() => {
292+
// reset the default value
293+
setDefaultShouldEmitPatchOnChange(true);
294+
});
295+
296+
test("readonly instances don't use the shouldEmitPatchOnChange option", () => {
297+
const fn = jest.fn();
298+
@register
299+
class MyViewExample extends ClassModel({ key: types.identifier, name: types.string }) {
300+
@snapshottedView({ shouldEmitPatchOnChange: fn })
301+
get slug() {
302+
return this.name.toLowerCase().replace(/ /g, "-");
303+
}
304+
}
305+
306+
const instance = MyViewExample.createReadOnly({ key: "1", name: "Test" });
307+
expect(instance.slug).toEqual("test");
308+
expect(fn).not.toHaveBeenCalled();
309+
});
310+
311+
test("observable instances don't emit a patch when shouldEmitPatchOnChange returns false", () => {
312+
const shouldEmitPatchOnChangeFn = jest.fn(() => false);
313+
const observableArray = observable.array<string>([]);
314+
315+
@register
316+
class MyViewExample extends ClassModel({ key: types.identifier, name: types.string }) {
317+
@snapshottedView({ shouldEmitPatchOnChange: shouldEmitPatchOnChangeFn })
318+
get arrayLength() {
319+
return observableArray.length;
320+
}
321+
}
322+
323+
const instance = MyViewExample.create({ key: "1", name: "Test" });
324+
expect(shouldEmitPatchOnChangeFn).toHaveBeenCalled();
325+
326+
const onPatchFn = jest.fn();
327+
onPatch(instance, onPatchFn);
328+
329+
runInAction(() => {
330+
observableArray.push("a");
331+
});
332+
333+
expect(onPatchFn).not.toHaveBeenCalled();
334+
});
335+
336+
test("observable instances do emit a patch when shouldEmitPatchOnChange returns true", () => {
337+
const shouldEmitPatchOnChangeFn = jest.fn(() => true);
338+
const observableArray = observable.array<string>([]);
339+
340+
@register
341+
class MyViewExample extends ClassModel({ key: types.identifier, name: types.string }) {
342+
@snapshottedView({ shouldEmitPatchOnChange: shouldEmitPatchOnChangeFn })
343+
get arrayLength() {
344+
return observableArray.length;
345+
}
346+
}
347+
348+
const instance = MyViewExample.create({ key: "1", name: "Test" });
349+
expect(shouldEmitPatchOnChangeFn).toHaveBeenCalled();
350+
351+
const onPatchFn = jest.fn();
352+
onPatch(instance, onPatchFn);
353+
354+
runInAction(() => {
355+
observableArray.push("a");
356+
});
357+
358+
expect(onPatchFn).toHaveBeenCalled();
359+
});
360+
361+
test("observable instances do emit a patch when shouldEmitPatchOnChange is undefined and setDefaultShouldEmitPatchOnChange hasn't been called", () => {
362+
const observableArray = observable.array<string>([]);
363+
364+
@register
365+
class MyViewExample extends ClassModel({ key: types.identifier, name: types.string }) {
366+
@snapshottedView()
367+
get arrayLength() {
368+
return observableArray.length;
369+
}
370+
}
371+
372+
const instance = MyViewExample.create({ key: "1", name: "Test" });
373+
374+
const onPatchFn = jest.fn();
375+
onPatch(instance, onPatchFn);
376+
377+
runInAction(() => {
378+
observableArray.push("a");
379+
});
380+
381+
expect(onPatchFn).toHaveBeenCalled();
382+
});
383+
384+
test("observable instances do emit a patch when shouldEmitPatchOnChange is undefined and setDefaultShouldEmitPatchOnChange was passed true", () => {
385+
setDefaultShouldEmitPatchOnChange(true);
386+
387+
const observableArray = observable.array<string>([]);
388+
389+
@register
390+
class MyViewExample extends ClassModel({ key: types.identifier, name: types.string }) {
391+
@snapshottedView()
392+
get arrayLength() {
393+
return observableArray.length;
394+
}
395+
}
396+
397+
const instance = MyViewExample.create({ key: "1", name: "Test" });
398+
399+
const onPatchFn = jest.fn();
400+
onPatch(instance, onPatchFn);
401+
402+
runInAction(() => {
403+
observableArray.push("a");
404+
});
405+
406+
expect(onPatchFn).toHaveBeenCalled();
407+
});
408+
409+
test("observable instances don't emit a patch when shouldEmitPatchOnChange is undefined and setDefaultShouldEmitPatchOnChange was passed false", () => {
410+
setDefaultShouldEmitPatchOnChange(false);
411+
412+
const observableArray = observable.array<string>([]);
413+
414+
@register
415+
class MyViewExample extends ClassModel({ key: types.identifier, name: types.string }) {
416+
@snapshottedView()
417+
get arrayLength() {
418+
return observableArray.length;
419+
}
420+
}
421+
422+
const instance = MyViewExample.create({ key: "1", name: "Test" });
423+
424+
const onPatchFn = jest.fn();
425+
onPatch(instance, onPatchFn);
426+
427+
runInAction(() => {
428+
observableArray.push("a");
429+
});
430+
431+
expect(onPatchFn).not.toHaveBeenCalled();
432+
});
433+
});
288434
});

src/class-model.ts

+38-18
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@ export interface SnapshottedViewOptions<V, T extends IAnyClassModelType> {
4949
/** A function for converting the view value to a snapshot value */
5050
createSnapshot?: (value: V) => any;
5151

52+
/** A function that determines whether the view should emit a patch when it changes. */
53+
shouldEmitPatchOnChange?: (node: Instance<T>) => boolean;
54+
5255
/** A function that will be called when the view's reaction throws an error. */
5356
onError?: (error: any) => void;
5457
}
@@ -303,24 +306,26 @@ export function register<Instance, Klass extends { new (...args: any[]): Instanc
303306
return {
304307
afterCreate() {
305308
for (const view of klass.snapshottedViews) {
306-
reactions.push(
307-
reaction(
308-
() => {
309-
const value = self[view.property];
310-
if (view.options.createSnapshot) {
311-
return view.options.createSnapshot(value);
312-
}
313-
if (Array.isArray(value)) {
314-
return value.map(getSnapshot);
315-
}
316-
return getSnapshot(value);
317-
},
318-
() => {
319-
self.__incrementSnapshottedViewsEpoch();
320-
},
321-
{ equals: comparer.structural, onError: view.options.onError },
322-
),
323-
);
309+
if (view.options.shouldEmitPatchOnChange?.(self) ?? defaultShouldEmitPatchOnChange) {
310+
reactions.push(
311+
reaction(
312+
() => {
313+
const value = self[view.property];
314+
if (view.options.createSnapshot) {
315+
return view.options.createSnapshot(value);
316+
}
317+
if (Array.isArray(value)) {
318+
return value.map(getSnapshot);
319+
}
320+
return getSnapshot(value);
321+
},
322+
() => {
323+
self.__incrementSnapshottedViewsEpoch();
324+
},
325+
{ equals: comparer.structural, onError: view.options.onError },
326+
),
327+
);
328+
}
324329
}
325330
},
326331
beforeDestroy() {
@@ -554,3 +559,18 @@ export function getPropertyDescriptor(obj: any, property: string) {
554559
export const isClassModel = (type: IAnyType): type is IClassModelType<any, any, any> => {
555560
return (type as any).isMQTClassModel;
556561
};
562+
563+
let defaultShouldEmitPatchOnChange = true;
564+
565+
/**
566+
* Sets the default value for the `shouldEmitPatchOnChange` option for
567+
* snapshotted views.
568+
*
569+
* If a snapshotted view does not have a `shouldEmitPatchOnChange`
570+
* function defined, this value will be used instead.
571+
*
572+
* @param value - The new default value for the `shouldEmitPatchOnChange` option.
573+
*/
574+
export function setDefaultShouldEmitPatchOnChange(value: boolean) {
575+
defaultShouldEmitPatchOnChange = value;
576+
}

0 commit comments

Comments
 (0)