Groups multiple signal writes so that dependent effects and memos run once, after all the writes inside the batch complete — instead of once per write.
import { batch } from '@barefootjs/client'
batch<T>(fn: () => T): TReturns the value produced by fn.
#Default behavior (no batch)
BarefootJS propagates updates synchronously: each setter call immediately re-runs every subscriber. This keeps reads-after-writes predictable — after a setter returns, derived memos, effects, and the DOM already reflect the new value.
The cost is that writing N signals that share a subscriber re-runs that subscriber N times, and the subscriber briefly observes intermediate states where some signals are updated and others are not:
const [x, setX] = createSignal(40)
const [y, setY] = createSignal(60)
createEffect(() => {
// depends on both x and y
send({ x: x(), y: y() })
})
setX(70) // effect runs — observes x=70, y=60 (intermediate)
setY(30) // effect runs again — observes x=70, y=30Beyond its initial run on creation, the effect ran twice more — once per write —
and saw a transient x=70, y=60 state.
#With batch
batch(() => {
setX(70)
setY(30)
})
// effect runs once, observing x=70, y=30Inside batch, writes are collected and dependent subscribers are de-duplicated,
so each runs exactly once after the batch ends — and never observes an
intermediate, half-updated state.
#When to use
When a single handler updates several signals that feed shared effects/memos,
batch collapses the work into one update pass — and keeps the subscriber from
running while a cross-field invariant is temporarily broken:
const reset = () => {
batch(() => {
setName('')
setEmail('')
setAge(0)
// ...20 more fields
})
// every subscriber ran once, not once-per-field
}#Caveats
#Derived values are stale *inside* the batch
batch defers the work that recomputes derived values. Plain signal reads return
the new value immediately, but memos and effect-driven values stay stale until
the batch ends:
const [n, setN] = createSignal(1)
const doubled = createMemo(() => n() * 2)
batch(() => {
setN(10)
n() // 10 — plain signal read is fresh
doubled() // 2 — STALE; the memo hasn't recomputed yet
})
doubled() // 20 — recomputed after the batch endsIf you need the recomputed value, read it after the batch.
#`await` escapes the batch
batch only covers the synchronous portion of fn. Wrapping an async
function in batch groups only the writes before the first await — everything
after runs ungrouped, and the promise batch returns is easy to leave floating.
Instead, wrap each synchronous group of writes in its own batch, with await
between the groups:
const onSubmit = async () => {
batch(() => {
setLoading(true)
setError(null)
})
try {
const result = await save()
batch(() => {
setLoading(false)
setResult(result)
})
} catch (err) {
batch(() => {
setLoading(false)
setError(err)
})
}
}#Note
batch is an opt-in optimization. Forgetting it is never a correctness bug —
code still works, just with extra subscriber runs. Reach for batch when a
handler writes many signals that share subscribers, or when an effect must not
observe a partially-updated state.