Common Patterns
Async data loading with flow
import { flow, makeObservable } from "@fobx/core"
class DataStore {
data: Item[] = []
loading = false
error: string | null = null
constructor() {
makeObservable(this, {
annotations: {
data: "observable",
loading: "observable",
error: "observable",
fetchData: "flow",
},
})
}
*fetchData() {
this.loading = true
this.error = null
try {
const res = yield fetch("/api/items")
if (!res.ok) throw new Error(`HTTP ${res.status}`)
this.data = yield res.json()
} catch (e) {
this.error = String(e)
} finally {
this.loading = false
}
}
}
Form state management
import { computed, observable } from "@fobx/core"
const form = observable({
email: "",
password: "",
submitted: false,
get emailError() {
if (!this.email) return "Required"
if (!this.email.includes("@")) return "Invalid email"
return null
},
get passwordError() {
if (!this.password) return "Required"
if (this.password.length < 8) return "Too short"
return null
},
get isValid() {
return !this.emailError && !this.passwordError
},
submit() {
this.submitted = true
if (!this.isValid) return
// ... submit logic
},
})
Undo/redo with snapshots
import { observable, runInTransaction } from "@fobx/core"
function createUndoable<T extends object>(initial: T) {
const state = observable(structuredClone(initial))
const history: T[] = [structuredClone(initial)]
let pointer = 0
return {
state,
commit() {
pointer++
history.length = pointer
history.push(structuredClone(state) as T)
},
undo() {
if (pointer <= 0) return
pointer--
runInTransaction(() => Object.assign(state, history[pointer]))
},
redo() {
if (pointer >= history.length - 1) return
pointer++
runInTransaction(() => Object.assign(state, history[pointer]))
},
get canUndo() {
return pointer > 0
},
get canRedo() {
return pointer < history.length - 1
},
}
}
If you want canUndo and canRedo to participate in reactivity, store the
history pointer in an observable (for example an observableBox) or wrap the
returned controller in observable().
Debounced reaction
import { reaction } from "@fobx/core"
function debouncedReaction<T>(
expression: () => T,
effect: (value: T) => void,
delay: number,
) {
let timeoutId: ReturnType<typeof setTimeout> | undefined
return reaction(expression, (value) => {
clearTimeout(timeoutId)
timeoutId = setTimeout(() => effect(value), delay)
})
}
// Usage
const stop = debouncedReaction(
() => searchStore.query,
(query) => searchStore.search(query),
300,
)
Local observable state (vanilla JS)
For UI components without a framework binding, pair an observable with
autorun:
import { autorun, observable } from "@fobx/core"
function createCounter(element: HTMLElement) {
const state = observable({ count: 0 })
const stop = autorun(() => {
element.textContent = `Count: ${state.count}`
})
element.addEventListener("click", () => state.count++)
return { dispose: stop }
}
Conditional tracking
Dependencies are only tracked for branches that execute:
import { computed, observableBox } from "@fobx/core"
const useMetric = observableBox(true)
const tempC = observableBox(20)
const tempF = observableBox(68)
const displayTemp = computed(() =>
useMetric.get() ? `${tempC.get()}°C` : `${tempF.get()}°F`
)
// When useMetric=true, only tempC is tracked
// When useMetric=false, only tempF is tracked
Differences from MobX
FobX is inspired by MobX but makes different design choices:
| Feature | FobX | MobX |
|---|---|---|
| Decorators | Not supported | Supported |
observable() target |
Objects, class instances, arrays, maps, sets | Objects, arrays, maps, sets |
| Proxy requirement | Used for observable arrays | Optional (with fallback) |
| Scheduler | Synchronous, epoch-based | Synchronous, derivation-based |
| Bundle size | ~5 KB gzipped | ~16 KB gzipped |
| Computed caching | Suspended when unobserved | Always cached once created |
| Global state | Shared via Symbol.for |
Instance-based |
FobX intentionally omits some MobX features to stay small and fast:
- No decorator support
- No
extendObservable - No
intercept/observelisteners - No
spy/tracedebugging tools - No
toJS(usestructuredCloneor manual serialization)