observable

observable() turns plain objects and class instances into reactive objects whose property reads are tracked and property writes trigger reactions. For an existing instance with an explicit annotations map, use makeObservable().

Signature

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.

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)

If you explicitly set defaultAnnotation to a data annotation ("observable", "observable.ref", "observable.shallow", or "none"), that override still applies to own fields, including function-valued callback properties stored directly on the object.

The fix in this area was narrower: it prevents that data default from leaking onto inherited prototype methods. Class methods and generators still infer to "transaction" or "flow" unless you annotate them explicitly.

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

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:

const store = observable(
  {
    coords: { x: 0, y: 0 },
    get total() {
      return this.coords.x + this.coords.y
    },
  },
  {
    annotations: {
      coords: ["observable", "structural"],
      total: ["computed", (a, b) => Math.abs(a - b) < 0.01],
    },
  },
})

Inheritance

observable() supports class inheritance. Base class explicit annotations stay authoritative by property name when a subclass later calls observable(this) or makeObservable(this):

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).

Use makeObservable() when you want an explicit annotations map instead of inference. It keeps the same instance reference and is usually the better fit for class constructors with carefully curated reactive members.