reaction
reaction is a two-phase reaction: a tracked expression that produces a
value, and an effect that runs (untracked) whenever that value changes.
Signature
function reaction<T>(
expression: (dispose: Dispose) => T,
effect: (
value: T,
previousValue: T | typeof UNDEFINED,
dispose: Dispose,
) => void,
options?: ReactionOptions<T>,
): Dispose
interface ReactionOptions<T> {
name?: string
fireImmediately?: boolean
comparer?: EqualityComparison
}
Parameters
| Parameter | Type | Description |
|---|---|---|
expression |
(dispose) => T |
Tracked function — its return value is compared across runs |
effect |
(value, prev, dispose) => void |
Side-effect — runs untracked when the expression value changes |
options.name |
string |
Debug name |
options.fireImmediately |
boolean |
Run the effect once immediately with the initial value (default false) |
options.comparer |
EqualityComparison |
Custom equality check for the expression result |
When fireImmediately is true, the first callback receives UNDEFINED as
previousValue.
Returns a Dispose function.
Basic usage
import { observableBox, reaction } from "@fobx/core"
const temperature = observableBox(20)
const stop = reaction(
() => temperature.get(),
(current, previous) => {
console.log(`Temperature changed from ${previous} to ${current}`)
},
)
temperature.set(25)
// prints: Temperature changed from 20 to 25
temperature.set(25)
// nothing — same value
stop()
Fire immediately
const stop = reaction(
() => temperature.get(),
(current) => {
console.log("temperature:", current)
},
{ fireImmediately: true },
)
// prints: temperature: 20 (immediately)
On that first fireImmediately run, previousValue is the exported UNDEFINED
sentinel.
Custom comparer
Use a custom comparer to control when the effect fires. The "structural"
comparer requires a one-time configure() call at app startup:
import { configure } from "@fobx/core"
configure({ comparer: { structural: myDeepEqual } })
const coords = observableBox({ x: 0, y: 0 })
const stop = reaction(
() => coords.get(),
(value) => console.log("moved to", value),
{ comparer: "structural" },
)
coords.set({ x: 0, y: 0 }) // no effect — structurally equal
coords.set({ x: 1, y: 0 }) // effect fires
stop()
Expression vs effect tracking
Only the expression function is tracked. The effect runs outside the tracking context, so observable reads inside the effect do not create dependencies:
const a = observableBox(1)
const b = observableBox(2)
const stop = reaction(
() => a.get(), // only a is tracked
() => {
console.log(b.get()) // b is read but NOT tracked
},
)
b.set(99) // does NOT trigger the reaction
a.set(2) // triggers the reaction → prints 99
stop()
Collection change detection
When the expression returns an observable collection, reaction compares by
internal change count rather than reference. This means mutations to the
collection are detected:
import { observableMap, reaction } from "@fobx/core"
const m = observableMap<string, number>()
const stop = reaction(
() => m,
() => console.log("map changed, size:", m.size),
)
m.set("a", 1) // prints: map changed, size: 1
m.set("b", 2) // prints: map changed, size: 2
stop()
Self-disposing
Both the expression and effect receive a dispose callback:
const stop = reaction(
(dispose) => {
const v = temperature.get()
if (v > 100) dispose() // stop if temperature exceeds 100
return v
},
(value) => console.log("temp:", value),
)