skip to content

MobX

MobX makes plain objects observable and re-runs whatever read them, so updates propagate without explicit subscriptions or dispatches. Interviewers ask how that tracking works and where the implicit reactivity makes behaviour harder to debug.

on this pageshow

questions

5

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.
open as a page

In MobX 6, why does assigning observable state after an await inside a store method trigger a strict-mode warning, and how do you fix it?

level: middleimportance: must knowfreq 42%

basics

~20 s

An action only covers the synchronous part of a function; code after await runs later, outside it. With the default enforceActions: "observed", changing observed state there logs a warning. Wrap the post-await writes in runInAction, or write the method as a flow generator.

open as a page

A React todo row shows a stale title after a MobX store updates todo.title; what do you check, and why must the component that renders the value be an observer?

level: seniorimportance: must knowfreq 45%

basics

~20 s

MobX tracks only reads made during an observer's own render. Check that the row is an observer, reads todo.title itself rather than receiving a pre-read string, is not rendered through a non-observer child or callback, and that title is observable.

open as a page

In MobX, when should derived data be a computed value rather than an autorun or reaction, and when does a computed actually cache?

level: middleimportance: should knowfreq 36%

basics

~20 s

Use computed for any value derived from state and reactions only for side effects such as saving or logging. A computed is lazy and caches its result only while something observes it; read outside a reaction, it recomputes on every access.

open as a page

A MobX autorun in a todo app re-runs more often than expected and keeps running after its screen closes; how do you find what it tracks, and how do you stop it?

level: seniorimportance: should knowfreq 30%

basics

~20 s

An autorun depends on everything it read in its last run, so serialising the whole list makes any field change re-run it. trace() or getDependencyTree shows those dependencies, reaction narrows them, and calling the returned disposer on unmount stops it.

open as a page