diff --git a/.changeset/smooth-glasses-search.md b/.changeset/smooth-glasses-search.md new file mode 100644 index 000000000..01ca749ae --- /dev/null +++ b/.changeset/smooth-glasses-search.md @@ -0,0 +1,5 @@ +--- +"mobx": minor +--- + +Add computed.shallow annotation diff --git a/docs/computeds.md b/docs/computeds.md index 8dcbb7b94..d38c57ea6 100644 --- a/docs/computeds.md +++ b/docs/computeds.md @@ -151,6 +151,14 @@ class Dimension { +
{🚀} **Tip:** `computed.shallow` for comparing output shallowly + +If the output of a computed value that is a collection with all items identical to the collection from previous computation doesn't need to notify observers, `computed.shallow` can be used. It will do size check and reference equality check for each item, rather than a reference equality check of the collection, before notifying observers. + +In practice, `computed.shalow` is useful only in limited situations. Only use it if changes in the underlying observables can still lead to the same contents in the collection, for example, if we were filtering a list of rectangles by whether they are in a given area, the rectangles included might stay the same even though the area changes. + +
+
{🚀} **Tip:** `computed.struct` for comparing output structurally If the output of a computed value that is structurally equivalent to the previous computation doesn't need to notify observers, `computed.struct` can be used. It will make a structural comparison first, rather than a reference equality check, before notifying observers. For example: diff --git a/docs/observable-state.md b/docs/observable-state.md index 821790d87..918a35f29 100644 --- a/docs/observable-state.md +++ b/docs/observable-state.md @@ -297,6 +297,7 @@ Note that it is possible to pass `{ proxy: false }` as an option to `observable` | `action` | Mark a method as an action that will modify the state. Check out [actions](actions.md) for more details. Non-writable. | | `action.bound` | Like action, but will also bind the action to the instance so that `this` will always be set. Non-writable. | | `computed` | Can be used on a [getter](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/get) to declare it as a derived value that can be cached. Check out [computeds](computeds.md) for more details. | +| `computed.shallow` | Like `computed`, but for collections. After recomputing, if the contents of the collection are equal to the previous result, no observers will be notified. | | `computed.struct` | Like `computed`, except that if after recomputing the result is structurally equal to the previous result, no observers will be notified. | | `true` | Infer the best annotation. Check out [makeAutoObservable](#makeautoobservable) for more details. | | `false` | Explicitly do not annotate this property. | diff --git a/packages/mobx/__tests__/v5/base/typescript-decorators.ts b/packages/mobx/__tests__/v5/base/typescript-decorators.ts index 9e6185dc4..c43271fda 100644 --- a/packages/mobx/__tests__/v5/base/typescript-decorators.ts +++ b/packages/mobx/__tests__/v5/base/typescript-decorators.ts @@ -207,7 +207,33 @@ test("computed setter should succeed", () => { t.equal(b.propX, 8) }) -test("typescript: parameterized computed decorator", () => { +test("@computed.shallow (TS)", () => { + class TestClass { + @observable.struct x = { a: 3 } + @observable.struct y = { b: 4 } + @computed.shallow + get array() { + return [this.x, this.y] + } + constructor() { + makeObservable(this) + } + } + + const t1 = new TestClass() + const changes: string[] = [] + const d = autorun(() => changes.push(JSON.stringify(t1.array))) + + t1.x = { a: 5 } // change + t.equal(changes.length, 2) + t1.x.a = 6 // no change + t.equal(changes.length, 2) + d() + + t.deepEqual(changes, ['[{"a":3},{"b":4}]', '[{"a":5},{"b":4}]']) +}) + +test("@computed.struct (TS)", () => { class TestClass { @observable x = 3 @observable y = 3 diff --git a/packages/mobx/src/api/computed.ts b/packages/mobx/src/api/computed.ts index 6ac31e8e0..82b52f939 100644 --- a/packages/mobx/src/api/computed.ts +++ b/packages/mobx/src/api/computed.ts @@ -17,6 +17,7 @@ import { import type { ClassGetterDecorator } from "../types/decorator_fills" export const COMPUTED = "computed" +export const COMPUTED_SHALLOW = "computed.shallow" export const COMPUTED_STRUCT = "computed.struct" export interface IComputedFactory extends Annotation, PropertyDecorator, ClassGetterDecorator { @@ -25,10 +26,14 @@ export interface IComputedFactory extends Annotation, PropertyDecorator, ClassGe // computed(fn, opts) (func: () => T, options?: IComputedValueOptions): IComputedValue + shallow: Annotation & PropertyDecorator & ClassGetterDecorator struct: Annotation & PropertyDecorator & ClassGetterDecorator } const computedAnnotation = createComputedAnnotation(COMPUTED) +const computedShallowAnnotation = createComputedAnnotation(COMPUTED_SHALLOW, { + equals: comparer.shallow +}) const computedStructAnnotation = createComputedAnnotation(COMPUTED_STRUCT, { equals: comparer.structural }) @@ -71,4 +76,5 @@ export const computed: IComputedFactory = function computed(arg1, arg2) { Object.assign(computed, computedAnnotation) +computed.shallow = createDecoratorAnnotation(computedShallowAnnotation) computed.struct = createDecoratorAnnotation(computedStructAnnotation)