MobX makeAutoObservable: where inference is enough and where to annotate
makeAutoObservable infers observable state, computed getters and auto-bound actions from a class shape. The annotations object is where the real engineering decisions live.
21 Feb 2026, 08:03 UTC

Two symptoms show up in MobX codebases that use class stores: a handler passed to a child component loses this, and a derived value recomputes on every render because nobody marked it computed. In MobX 6, makeAutoObservable removes most of that ceremony by inferring what each class member is. The interesting engineering decision is not whether to use it, but where to override its inference.
What inference actually decides
Calling makeAutoObservable(this) in a constructor walks the class shape and assigns a role to each member:
- Own fields that hold data become
observable. - Getters with no arguments become
computed. - Methods become
action, and are bound to the instance.
An observable is a value MobX watches. A computed is a cached derived value that MobX recomputes only when one of its observable dependencies changes. An action is a mutation MobX batches so reactions run once per action rather than once per assignment. The auto-binding matters in practice: store.addItem can be passed directly as a React prop without an arrow wrapper or a manual bind in the constructor.
The annotations object is the real lever
The second argument to makeAutoObservable is an annotations map that overrides inference per property. The overrides worth knowing:
shallow— the collection itself is observable, but its contents are not converted into observables.ref— the value is stored as-is and never made reactive.false— the property is excluded from MobX entirely.- Explicit
computedoraction— pin a role when inference guesses wrong.
By default MobX makes data deeply observable: assigning an array of plain objects converts each object into a proxy. That is convenient for small models and expensive for large ones.
Worked example: a cart with a shallow item list
import { makeAutoObservable, shallow } from 'mobx';
class CartStore {
items = [];
discount = 0;
constructor() {
makeAutoObservable(this, {
items: shallow,
});
}
addItem(item) {
this.items.push(item);
}
get total() {
return this.items.reduce((sum, i) => sum + i.price, 0) * (1 - this.discount);
}
}
With items: shallow, pushing to the array or replacing it triggers reactions, but mutating item.price on an existing element does not, because MobX is not proxying the element objects. That is the correct choice when items are replaced rather than edited in place, and a bug when they are edited in place. discount stays a normal observable scalar, and total is a computed value that recomputes only when items or discount changes.
Limits and version assumptions
Inference is not a substitute for modeling. Deep observability on a large object graph costs proxy creation at construction time, so collections of immutable payloads are usually better as shallow or ref. TypeScript visibility also interacts with inference: private fields are not reliably inferred as observable across compiler configurations, so annotate them explicitly if you use private syntax. The API surface described here is MobX 6; the decorator-based model in MobX 5 behaves differently, and defaults have changed between major versions. Confirm the behavior against the version you actually install.
How to check it in your project
- Confirm the installed version with
npm ls mobx, or read the entry inpackage.json. - Write a minimal class with one field, one getter and one method, and call
makeAutoObservable(this)in the constructor. - Wrap a log in
autorun, mutate the field, and confirm the log fires once per action and not when an unrelated field changes. - Add a
shallowannotation to a list and confirm that mutating an element property no longer triggers the reaction, while pushing a new element still does.
If the reaction fires more often than expected, an annotation is missing. If it fires less often than expected, something is annotated shallow or ref that should be deep. That check is cheap and settles the question faster than reasoning about it.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.