observable / makeObservable

observable() and makeObservable() turn objects into reactive objects whose property reads are tracked and property writes trigger reactions.


observable()

Creates a reactive object from a plain object or class instance. Annotations are auto-inferred when not provided.

For arrays, maps, and sets, observable() delegates to observableArray, observableMap, and observableSet.

Signature

function observable<T extends object>(
  target: T,
  options?: ObservableOptions<T>,
): T

interface ObservableOptions<T extends object> {
  name?: string
  defaultAnnotation?: AnnotationString
  annotations?: Partial<AnnotationsMap<T>>
  inPlace?: boolean
  ownPropertiesOnly?: boolean
}

Basic usage

import { autorun, observable } from "@fobx/core"

const store = observable({
  count: 0,
  items: ["a", "b"],
  get total() {
    return this.items.length
  },
  increment() {
    this.count++
  },
})

autorun(() => console.log(store.count))
// prints: 0

store.increment()
// prints: 1

How inference works

When no annotations are provided, observable() infers them:

Property kind Inferred annotation
Own data property "observable" (deep)
Getter accessor "computed"
Function value "transaction"
Generator function value "flow"

Options

Option Type Default Description
name string (generated) Debug name
annotations AnnotationsMap (inferred) Override annotations for specific properties
defaultAnnotation AnnotationString "observable" Default for data properties
inPlace boolean false Mutate the source object instead of copying
ownPropertiesOnly boolean false Install all descriptors on instance (skip prototype)

inPlace mode

By default, observable() returns a new observable copy when you pass a plain object. With inPlace: true, the original plain object is mutated. Class instances are always mutated in place:

const source = { count: 0 }
const store = observable(source, { inPlace: true })

console.log(source === store) // true — same reference

makeObservable()

Makes an object reactive using explicit annotations. It is most commonly used in a class constructor. Unlike observable(), it does not auto-infer annotations — you must provide an explicit annotations map.

Signature

function makeObservable<T extends object>(
  target: T,
  options?: MakeObservableOptions<T>,
): T

interface MakeObservableOptions<T extends object> {
  name?: string
  annotations?: AnnotationsMap<T>
  ownPropertiesOnly?: boolean
}

Basic usage

import { autorun, makeObservable } from "@fobx/core"

class TodoStore {
  todos: string[] = []
  get count() {
    return this.todos.length
  }
  addTodo(text: string) {
    this.todos.push(text)
  }

  constructor() {
    makeObservable(this, {
      annotations: {
        todos: "observable",
        count: "computed",
        addTodo: "transaction",
      },
    })
  }
}

const store = new TodoStore()
autorun(() => console.log("count:", store.count))
// prints: count: 0

store.addTodo("Buy milk")
// prints: count: 1

Explicit annotations

Every property you want to be reactive must be listed in the annotations map. Properties not listed are left untouched — there is no need to mark them:

class Store {
  id = "fixed" // not listed → not observable
  count = 0
  get doubled() {
    return this.count * 2
  }

  constructor() {
    makeObservable(this, {
      annotations: {
        count: "observable",
        doubled: "computed",
      },
    })
  }
}

Annotations reference

Annotation Description
"observable" Deep observable — collections and plain objects are recursively converted
"observable.ref" Reference-only observable — value is stored as-is, no deep conversion
"observable.shallow" Shallow observable — collections track mutations but items are not converted
"computed" Computed derived value
"transaction" Action — wrapped in a transaction automatically
"transaction.bound" Bound action — this is bound to the instance
"flow" Flow — generator-based async action
"flow.bound" Bound flow
"none" Excluded from reactive system
false Skip this property (observable() only — overrides auto-inference)

Annotation with custom comparer

Use the array form [annotation, comparer] to set a custom equality check. The "structural" comparer requires a one-time configure() call at app startup:

makeObservable(this, {
  annotations: {
    coords: ["observable", "structural"], // structural equality
    total: ["computed", (a, b) => Math.abs(a - b) < 0.01],
  },
})

Inheritance

makeObservable() supports class inheritance. Each class in the chain calls makeObservable(this) with its own annotations:

class Base {
  value = 0
  constructor() {
    makeObservable(this, {
      annotations: { value: "observable" },
    })
  }
}

class Derived extends Base {
  extra = ""
  constructor() {
    super()
    makeObservable(this, {
      annotations: { extra: "observable" },
    })
  }
}

Prototype members are annotated once per prototype and reused across instances.

When multiple constructors in the same chain annotate the same instance, base class explicit annotations stay authoritative by property name. A subclass can add new reactive members, but it cannot reinterpret a base key from "computed" to "none", or from "none" to "transaction", unless the base class itself left that key unclaimed.

This applies to both makeObservable(this) and observable(this) on class instances, which is what keeps hooks like ViewModel.update() plain even when a subclass later calls observable(this).


observable() vs makeObservable()

Feature observable() makeObservable()
Input Plain object or class instance Any non-collection object
Returns New plain-object copy by default; class instances keep the same reference Same instance
Auto-inference Yes No — explicit annotations required
Prototype methods Supported (class instances) Supported
Inheritance Supported (class instances); base explicit annotations stay authoritative Supported; base explicit annotations stay authoritative
inPlace option Yes N/A (always in-place)
Best for Simple stores, configuration Classes or objects with explicit annotations