skip to content

In MobX 6, what is the difference between makeObservable with annotations and makeAutoObservable, and when can you not use makeAutoObservable?

level: juniorimportance: must knowfreq 42%

answer

  1. explicit list versus inference
  2. fields, getters, functions
  3. called in the constructor
  4. the overrides argument
  5. inheritance is the catch

basics

~20 s

makeObservable(this, { ... }) annotates only the members you list; makeAutoObservable(this) infers them: fields become observable, getters computed, functions autoAction, generators flow. makeAutoObservable cannot be used on classes that have a superclass or are subclassed.

solid answer

~50 s

Both are called in a class constructor (or on a plain object) and turn existing members into MobX members. `makeObservable(this, { todos: observable, remaining: computed, add: action })` touches **only** the members you annotate, so a new field that nobody adds to the map stays plain. `makeAutoObservable(this, overrides?)` **infers** annotations: own properties become `observable`, getters `computed`, setters `action`, functions `autoAction` and generator functions `flow`; the `overrides` map changes individual members, and `false` excludes one, such as an id. The trade-off is maintenance versus control: auto inference means new members are covered automatically, while explicit maps document intent and allow finer annotations like `observable.ref`. The hard limit is inheritance: the MobX docs state that `makeAutoObservable` cannot be used on classes that have a superclass or are subclassed; those classes use `makeObservable`, with `override` for members redefined in a subclass.

code

ts · 21 lines
ts
import { makeAutoObservable } from 'mobx'

type Todo = { id: string; title: string; done: boolean }

export class TodoStore {
  readonly id = 'todos'
  todos: Todo[] = []

  constructor() {
    // fields -> observable, getters -> computed, functions -> autoAction
    makeAutoObservable(this, { id: false })
  }

  get remaining() {
    return this.todos.filter((t) => !t.done).length
  }

  add(title: string) {
    this.todos.push({ id: crypto.randomUUID(), title, done: false })
  }
}

go deeper

for a junior

Recall the four core annotations, observable, computed, action and flow, and that makeAutoObservable infers them while makeObservable needs an explicit map.

for a middle

Explain the inference table, what autoAction means for functions called during render, and how the overrides map excludes or refines members.

for a senior

Choose per store: explicit maps for class hierarchies and precision annotations, auto inference for flat stores, and watch for unannotated new fields.

for a principal

Set a codebase rule for annotation style and inheritance in stores, since mixing styles across a hierarchy is unsupported and hard to refactor later.

## What these functions do MobX 6 makes existing JavaScript objects reactive by **annotating** their members. An annotation tells MobX what a member is: - `observable` — a field that stores state and whose reads are tracked; - `computed` — a getter that derives a value from observables and caches it; - `action` — a method that modifies state, run as a batched transaction; - `flow` — a generator function used for asynchronous processes. The two functions differ in **who decides the annotations**. Both are normally called once, unconditionally, at the end of setting up fields in a class constructor, with `this` as the target; both also work on plain objects. ## `makeObservable`: you list them ```ts import { makeObservable, observable, computed, action } from 'mobx' type Todo = { id: string; title: string; done: boolean } class TodoStore { todos: Todo[] = [] constructor() { makeObservable(this, { todos: observable, remaining: computed, add: action, toggle: action, }) } get remaining() { return this.todos.filter((t) => !t.done).length } add(title: string) { this.todos.push({ id: crypto.randomUUID(), title, done: false }) } toggle(id: string) { const t = this.todos.find((x) => x.id === id) if (t) t.done = !t.done } } ``` **Only the listed members** are affected. If someone later adds `filter = 'all'` and forgets to annotate it, components will not react to changes of `filter` — a silent bug. In exchange you get precise control, for example `observable.ref` for a field holding immutable data, or `observable.shallow` for a collection whose items should stay plain. ## `makeAutoObservable`: MobX infers them `makeAutoObservable(this, overrides?, options?)` applies these inference rules: | Member | Inferred annotation | |---|---| | own property | `observable` (deep by default) | | getter | `computed` | | setter | `action` | | function | `autoAction` | | generator function | `flow` | `autoAction` is a special internal annotation: MobX decides at call time whether the function behaves as an action (when called from an event handler) or as a derivation (when called while rendering or computing), so helper lookups called from an `observer` component are still tracked. The `overrides` argument adjusts individual members — `{ id: false }` leaves a read-only id plain, `{ items: observable.shallow }` narrows one field. With `makeAutoObservable` the store above shrinks to one line in the constructor, and new members are covered automatically. ## When `makeAutoObservable` is off the table The docs are explicit: **`makeAutoObservable` cannot be used on classes that have a superclass or are subclassed.** For class hierarchies: 1. Each class calls `makeObservable` for the members it declares itself. 2. A subclass that redefines an inherited `action`, `computed`, `flow` or `action.bound` annotates it with `override`. 3. Only members defined on the prototype (methods, getters) can be overridden this way; fields cannot change annotation in a subclass. Other limitations apply to both functions: - They only see properties that already exist, so class fields must be initialised (or the compiler configured for spec-compliant class fields). - They must be called unconditionally, because MobX caches inference results per class. - ECMAScript `#private` fields are not supported; TypeScript `private` fields can be annotated by passing their names as a generic argument to `makeObservable`. ## Plain objects and decorators The same inference works without classes: `makeAutoObservable({ todos: [], get remaining() { ... }, add(title) { ... } })` returns an observable object with the same rules applied, which suits factory-function stores. The docs also list decorators (`@observable`, `@computed`, `@action`) as an alternative to calling `makeObservable` in the constructor; mixing decorators and annotation maps within one inheritance chain is not supported, so a codebase should pick one style per hierarchy. ## How to choose - **Plain stores without inheritance:** `makeAutoObservable` is the common default; use `overrides` for the few members that need something else. - **Class hierarchies or shared base stores:** `makeObservable` in each class. - **Libraries or very large stores where intent should be reviewable:** explicit maps make every reactive member visible in one place. ## Common mistakes - Annotating a method that only *reads* state as `action`: actions are untracked, so a component calling it during render would stop reacting to what it reads. - Switching a base class from `makeObservable` to `makeAutoObservable` to save lines, which the docs rule out once the class is subclassed. - Adding a field after the constructor call and expecting it to be observable.

  • In MobX, why should a lookup method such as findById not be annotated as action?
    Actions run untracked: reads inside them do not create subscriptions. If an `observer` component calls `store.findById(id).title` during render and `findById` is an action, the component stops reacting to the todo it found. Leave lookups unannotated, or rely on `makeAutoObservable`'s `autoAction`, which behaves as a derivation when called during rendering.
  • A MobX store extends a BaseStore. How do you annotate it?
    `makeAutoObservable` is not allowed on classes that have a superclass or are subclassed, so each class calls `makeObservable(this, { ... })` for the members it declares. If the subclass redefines an inherited action, computed, flow or action.bound, it annotates that member with `override` in its own map.

saying these in an interview costs you the question

  • makeObservable automatically picks up every field, like makeAutoObservable does.
  • makeAutoObservable works the same way on subclasses as on base classes.
  • Getters must be listed as action to be cached in makeAutoObservable.
  • Marking read-only lookup methods as action is harmless.
  • Fields added after the constructor call become observable automatically.