Behaviour

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])

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:

EntryMeaning
$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 effectComponent-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.

Live
$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) { … }
TriggerMount, unmount, interval, watched atoms.Explicit call from a handler (onClick) or expression.
IdentityAnonymous — keyed on source location.Named — referenced by identifier.
ArgumentsNone.Positional / named params bound inside the body.
Return valueNone.Optional — observable when called as $x = name(args).
CleanupRegistered 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.

Read the guide → Advanced

JavaScript interactions

Inline JavaScript — browser APIs, host globals, and custom-event integration.

Open the guide →
Data

HTTP & resources

The $http({…}) resource bag, refetch(), and the onDone callback.

View the guide →