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)