Effects + targets
Effects are non-visual nodes that react to lifecycle, time, visibility, and external events.
What effects are
Effects are non-visual nodes that react to changes over time or outside the render tree. They live alongside your UI nodes in the IR, but they do not produce DOM.
This makes effects easy to reason about: they are part of the same tree, follow the same lifecycle, and can be mounted, updated, or removed together with the UI that depends on them.
Core effect types
Thorm includes effect builders for the most common reactive patterns. They let you declare background work without leaving the IR tree.
- onMount(...) runs actions once after the UI mounts; good for bootstrapping data or timers.
- watch(expr, actions) reacts to expression changes; ideal for derived state, autosave, or debounced work.
- every(ms, actions) runs actions on a fixed interval; use for polling or counters.
- after(ms, actions) runs a one-shot delayed action; useful for timeouts or deferred UI updates.
- onVisible(...) reacts when a target enters the viewport; great for lazy loading or analytics.
- onLeftViewport(...) reacts when a target leaves the viewport; useful for cleanup or pause logic.
Effects are composable. You can mount multiple effects next to a UI subtree, and each effect can update state or trigger side effects.
Event and stream effects
Beyond lifecycle and timing, Thorm lets you react to DOM events and streamed data via effect triggers.
- onWindow(event, actions) listens to window events like resize or scroll.
- onDocument(event, actions) listens at document scope; useful for global key handlers.
- onSelf(event, actions, options, target) attaches to a specific element; good for form submit or custom input handling.
- onSse(url, actions, event, parse, ...) listens to Server-Sent Events; ideal for live feeds and streaming updates.
- effect(triggers, actions, target) is the low-level builder when you need custom trigger combinations.
These effects let you bind UI behavior to the environment: window, document, specific elements, or streaming endpoints.
Common browser events you can attach
You can listen to any standard DOM event by passing its name into onWindow(), onDocument(), or onSelf(). The list below is a starting point, not a complete catalog.
- Window: resize, scroll, focus, blur, online, offline, popstate, hashchange
- Document: keydown, keyup, visibilitychange, click
- Element: input, change, submit, pointerdown, pointerup, mouseenter, mouseleave, animationend
If the browser fires the event, you can likely attach it here. Use targets to scope it and keep listeners predictable.
Option A example: listen to window resize and keep an atom in sync with the current width. This is a minimal, copy-paste friendly setup for your own tests.
Another Example: window scroll demo
Targets and scope
Some effects need a target: the window, the document, or a specific element. Targets make effects precise and predictable.
- windowTarget binds to global window events
- documentTarget binds to document-level events
- selectorTarget targets a specific element by selector
Use the narrowest target you can. It keeps listeners scoped and makes cleanup safe when nodes are removed.
Patterns that scale
- Use every() to build timers, polling loops, and heartbeat counters.
- Use watch() to keep derived atoms in sync or trigger background work when state changes.
- Combine watch() with debounce/throttle for expensive operations (search, analytics, autosave).
- Use onVisible() with selectorTarget() for lazy loading or prefetch on scroll.
Effects are most powerful when they are isolated and declarative. They should read input expressions and emit actions, without reaching into unrelated state.
Example: interval-driven state
This example uses every() to update an atom once per second. The UI reads $tick and updates automatically.
The important idea is that the timer is declared inside the IR tree. There is no manual setInterval to manage, and cleanup is tied to the node lifecycle.
Example: watch with debounce and throttle
This example uses watch() to react to changes in $count and update a derived atom ($label). The watch() call subscribes to an expression (read($count)), and whenever that expression changes it runs a list of actions. Here, the action is set($label, concat('Count: ', read($count)), true).
Debounce and throttle shape how often the effect runs. Debounce waits until changes settle, throttle limits how often updates can happen. In this example the sliders change debounceMs and throttleMs at runtime, so you can feel the difference between βwait for quietβ vs βlimit frequency.β
The result is clean separation: button clicks only change $count, while watch() handles all derived updates. That keeps event handlers simple and moves reactive behavior into one place.
SSR note
Effects are intended to run on the client after hydration. During SSR, the UI should render based on initial atom values, while effects remain inert.
Because SSR behavior has not been fully tested in this setup, treat effect execution as client-only and verify your pages if you rely on SSR in production.
Practical guidance
- Keep effects focused: one effect should do one job and update a small number of atoms.
- Use debounce/throttle to avoid overloading expensive workflows.
- Prefer selectorTarget() when you can scope an effect to a specific element.
Effects are the bridge between your UI and the outside world. Declare them explicitly and keep them close to the nodes they serve.