computed

computed creates a derived value that is automatically recalculated when its dependencies change. When observed, the result is cached and only recomputed when needed.

Signature

function computed<T>(
  fn: () => T,
  options?: ComputedOptions<T>,
): Computed<T>

interface Computed<T> {
  get(): T
  set(value: T): void // only if options.set is provided
  dispose(): void
}

interface ComputedOptions<T> {
  name?: string
  comparer?: EqualityComparison
  set?: (value: T) => void
  bind?: unknown
}

Parameters

Parameter Type Description
fn () => T Derivation function. Must be pure — no side effects.
options.name string Debug name (defaults to Computed@<id>)
options.comparer EqualityComparison How to check if recomputed value differs from cached
options.set (value: T) => void Optional setter (e.g. for two-way bindings)
options.bind unknown this context for the derivation function

Basic usage

import { autorun, computed, observableBox } from "@fobx/core"

const price = observableBox(10)
const quantity = observableBox(3)

const total = computed(() => price.get() * quantity.get())

const stop = autorun(() => {
  console.log("total:", total.get())
})
// prints: total: 30

price.set(20)
// prints: total: 60

stop()

Caching behavior

A computed has two modes:

Unobserved (suspended)

When no reaction is observing the computed, each get() call recomputes from scratch outside an active batch. The computed holds no subscriptions and uses no long-lived cache.

const a = observableBox(1)
let runs = 0
const doubled = computed(() => {
  runs++
  return a.get() * 2
})

doubled.get() // runs = 1
doubled.get() // runs = 2 — no cache

Observed (active)

When at least one reaction observes the computed, it activates caching:

const a = observableBox(1)
let runs = 0
const doubled = computed(() => {
  runs++
  return a.get() * 2
})

const stop = autorun(() => doubled.get())
// runs = 1 (initial)

doubled.get() // still 1 — served from cache
a.set(2) // runs = 2 — recomputed because dependency changed

stop()
// computed suspends: cache discarded

Firewall effect

If a computed recomputes but produces the same output (according to its comparer), downstream reactions are not re-run:

const x = observableBox(3)
const isPositive = computed(() => x.get() > 0)

let effectRuns = 0
const stop = autorun(() => {
  effectRuns++
  isPositive.get()
})
// effectRuns = 1

x.set(5) // isPositive still true → effectRuns stays 1
x.set(-1) // isPositive now false → effectRuns = 2

stop()

Custom comparer

The "structural" comparer requires a one-time configure() call at app startup to provide the equality function:

import { configure } from "@fobx/core"
import { equals } from "fast-equals"

configure({ comparer: { structural: equals } })

Then use it on a computed:

const coords = observableBox({ x: 0, y: 0 })

const rounded = computed(
  () => ({
    x: Math.round(coords.get().x),
    y: Math.round(coords.get().y),
  }),
  { comparer: "structural" },
)

Custom setter

The set option lets you define write-back logic. This is rarely needed — most computeds are read-only derivations. Use sparingly for cases like unit conversions:

const celsius = observableBox(0)

const fahrenheit = computed(
  () => celsius.get() * 9 / 5 + 32,
  { set: (f) => celsius.set((f - 32) * 5 / 9) },
)

fahrenheit.set(212)
console.log(celsius.get()) // 100

Dispose

Call dispose() to drop the computed’s current upstream subscriptions and return it to an unobserved state:

const c = computed(() => heavy.get())
// ... later ...
c.dispose()

The computed itself still works after disposal. A later get() will recompute from scratch and re-establish dependencies if it becomes observed again.