Side effects.
$effect(() => { … }, [...deps]) runs declarative
side effects when its dependencies change — polling, clocks,
hydration, analytics, keyboard listeners — rather than when a
user clicks. The body is anonymous; the optional dependency array
decides when it fires.
Anatomy
An effect declaration has two parts: a body and an optional dependency array.
$effect(() => {
// body — side effects: state writes, $http(…), $emit(…), cleanup(…)
}, [...dependencies])
- The
$effect()call — effects are anonymous; there is no name and no return value. The runtime keys on the source location. - An arrow-function body that performs the side effect.
- An optional second argument — a dependency array — that controls when the body runs.
No array runs once on mount.
$effect(() => { … }) is exactly equivalent to
$effect(() => { … }, ["mount"]) — both run the
body a single time when the surrounding scope mounts.
Dependency entries
Each entry in the dependency array is one of the following:
| Entry | Meaning |
|---|---|
$atom |
Re-run the body whenever the named reactive atom changes. Mix multiple state triggers freely: [$query, $page] re-runs when either changes. |
"mount" |
Run the body once when the surrounding component (or top-level scope) mounts. |
"unmount" |
Run the body once when it unmounts. Useful for teardown / analytics on close. |
"every(N)" |
Re-run the body every N milliseconds. Driven by an interval; cleared automatically on unmount. |
"debounce(N)" |
Wrap the body with a trailing-edge debounce — the body only runs N ms after the last trigger. Combine with state / interval triggers. |
"throttle(N)" |
Wrap the body with a throttle — the body runs immediately, then suppresses further calls until N ms have elapsed (with a trailing call for the most recent state). |
Order inside the array does not matter. Only one rate-limit modifier
("debounce" or "throttle") takes effect per
declaration. A pure state-driven effect also runs once on mount, so it
observes the initial state.
Scope: top-level vs. component-local
An effect can live at the program top level
or inside a component body. The dependency-array
syntax is identical; only the lifecycle differs.
| Top-level effect | Component-local effect | |
|---|---|---|
| Mounted | Once, when the program first runs. | Once per component instance, the first time it renders. |
| Unmounted | When the program is replaced (setResponse / clear()). |
When the instance disappears from the render tree. |
| Multiplicity | One copy, regardless of who uses it. | One copy per instance — two App() calls = two
independent effects with independent timers / cleanups. |
Cleanup
Long-running listeners (timers, observers, event handlers, network
subscriptions) MUST be torn down so they don’t leak across
re-runs or unmount. Register a teardown with cleanup(fn)
from inside the body. A cleanup runs before each
re-run AND on unmount — this is where you clear timers,
remove listeners, and drop subscriptions.
$effect(() => {
const onKey = (e) => {
if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
e.preventDefault()
$paletteOpen = !$paletteOpen
}
}
document.addEventListener("keydown", onKey)
cleanup(() => document.removeEventListener("keydown", onKey))
}, ["mount"])
Timers
setTimeout, setInterval,
clearTimeout, and clearInterval are
first-class globals that behave like their JavaScript
counterparts and return a handle you can clear. Every handle is
runtime-tracked and torn down automatically on re-plan or unmount, so a
timer never outlives the program. Best practice: create timers
inside an effect (not at the top level,
where every render would re-create them) and clear them from the
effect’s cleanup.
For a simple repeating effect, prefer the "every(N)"
trigger. Reach for setInterval /
setTimeout when you need an imperative handle — a
debounced restart, or a one-shot delay.
$now = 0
$effect(() => {
let id = setInterval(() => { $now = $util.now() }, 1000)
cleanup(() => clearInterval(id))
}, ["mount"])
$app(Column([ Text(`Ticks: ${$now}`) ]))
Fetching data in an effect
Re-run an $http({…}) request when a watched atom
changes — the canonical search-as-you-type pattern. List the
atoms you watch plus a "debounce(N)" so keystrokes
don’t flood the network:
$query = ""
$effect(() => {
$results = $http({
url: "https://api.example.com/search",
query: { q: $query }
})
}, [$query, "debounce(300)"])
You do not need an effect for a one-shot fetch — a
top-level $results = $http({ … }) already fires once
on mount. For the “refresh a list after a write” case, prefer
the resource’s .onDone callback over an effect: it
fires every time the request settles, so you can refetch the list from
the write resource directly.
$orders = $http({ url: "https://api.example.com/orders" })
$create = $http({ url: "https://api.example.com/orders", method: "POST", body: { name: $name } })
$create.onDone = () => { $orders.refetch() }
Recipes
React to multiple atoms
$query = ""
$page = 1
$effect(() => {
$results = $http({
url: "https://api.example.com/search",
query: { q: $query, page: $page }
})
}, [$query, $page, "debounce(250)"])
Polling on an interval
$effect(() => {
$orders.refetch()
}, ["every(30000)"])
Autosave a draft
$draft = ""
$effect(() => {
$save = $http({
url: "https://api.example.com/draft",
method: "PUT",
body: { draft: $draft }
})
}, [$draft, "debounce(500)"])
Keyboard-shortcut listener
$effect(() => {
const onKey = (e) => {
if (e.key === "/" && !e.metaKey && !e.ctrlKey) {
e.preventDefault()
$searchOpen = true
}
}
document.addEventListener("keydown", onKey)
cleanup(() => document.removeEventListener("keydown", onKey))
}, ["mount"])
IntersectionObserver for infinite scroll
$effect(() => {
const sentinel = document.querySelector("[data-sentinel]")
if (!sentinel) return
const observer = new IntersectionObserver((entries) => {
if (entries.some((e) => e.isIntersecting)) {
$page = $page + 1
}
})
observer.observe(sentinel)
cleanup(() => observer.disconnect())
}, ["mount"])
WebSocket subscription
$effect(() => {
const socket = new WebSocket("wss://example.com/ticker")
socket.addEventListener("message", (event) => {
$ticker = JSON.parse(event.data)
})
cleanup(() => socket.close())
}, ["mount"])
Debounced restart with an imperative timer
$searchTimer = null
function onType(q) {
clearTimeout($searchTimer)
$searchTimer = setTimeout(() => {
$results = $http({ url: `https://api.example.com/search?q=${q}` })
}, 300)
}
Effect vs. Action
An effect is dependency-triggered and returns nothing.
An action is a named
function triggered explicitly by the user.
$effect(() => { … }, […]) | function name(args) { … } | |
|---|---|---|
| Trigger | Mount, unmount, interval, watched atoms. | Explicit call from a handler (onClick) or expression. |
| Identity | Anonymous — keyed on source location. | Named — referenced by identifier. |
| Arguments | None. | Positional / named params bound inside the body. |
| Return value | None. | Optional — observable when called as $x = name(args). |
| Cleanup | Registered with cleanup(fn); fires on re-run + unmount. | Not applicable. |
Catching action errors with $util.onError
Register a program‑level error sink with $util.onError(fn).
When an action throws, the runtime calls fn({ error, source })
before its default logging — the place to surface a toast,
report to analytics, or set a fallback flag. Pass null to clear.
$util.onError(({ error, source }) => {
$toast.error("Something went wrong")
$console.warn("action failed in", source, error)
})
For render‑time failures inside a subtree, wrap it in an
ErrorBoundary, which
renders a friendly fallback card instead of breaking the page.
Subscriptions & realtime streams
Long‑lived subscriptions usually don’t need a hand‑written
effect: the realtime primitives $socket (WebSocket) and
$sse (Server‑Sent Events) open a connection, expose a
reactive bag (.connected / .last /
.messages), and tear themselves down on re‑plan or
disconnect — no manual cleanup(fn) required.
$feed = $socket({ url: "wss://example.com/live" })
// react to pushes:
$effect(() => { if ($feed.last) $items = [...$items, $feed.last] }, [$feed.last])
See the HTTP guide for the full
$socket / $sse API. When you do roll your own
subscription in an effect, always return teardown via
cleanup(fn), as shown above.
Next
Imperative functions triggered by events — the user-driven counterpart to effect.
JavaScript interactions
Inline JavaScript — browser APIs, host globals, and custom-event integration.
Open the guide → DataHTTP & resources
The $http({…}) resource bag, refetch(), and the onDone callback.