Skip to content

Effects

Reactive side effects for work that happens outside the signal graph.

Effects connect the reactive graph to the outside world. They run immediately, track the signals they read, and re-run when those dependencies change.

Use them for DOM APIs, timers, network coordination, logging, or any other work that should happen because state changed. Do not use them to compute values that could stay inside the reactive graph.

Working with Effects

Creating Effects

Use Effect.run for fire-and-forget effects and Effect.runWithDisposer when you need to stop the effect manually.

1open Xote
2
3let count = Signal.make(0)
4
5Effect.run(() => {
6 Console.log2("Count:", Signal.get(count))
7 None
8})

Dependency Tracking

Effects track dependencies automatically. Every Signal.get call inside the effect subscribes the effect to that signal. On each run, dependencies are cleared and tracked again.

1let showDetails = Signal.make(false)
2let name = Signal.make("Ada")
3let age = Signal.make(36)
4
5Effect.run(() => {
6 if Signal.get(showDetails) {
7 Console.log2("Name:", Signal.get(name))
8 Console.log2("Age:", Signal.get(age))
9 }
10 None
11})

Cleanup Callbacks

An effect can return Some(cleanupFn) or None. Cleanup runs before the next execution and when the effect is disposed.

1let url = Signal.make("/api/users")
2
3Effect.run(() => {
4 let currentUrl = Signal.get(url)
5 let controller = AbortController.make()
6
7 fetch(currentUrl, {"signal": controller##signal})->ignore
8
9 Some(() => controller##abort())
10})

Disposing Effects

When you need explicit teardown, use Effect.runWithDisposer. It returns an object with a dispose() method.

1let disposer = Effect.runWithDisposer(() => {
2 Console.log(Signal.get(count))
3 None
4})
5
6disposer.dispose()

Avoiding Dependencies

If you need a value inside an effect without subscribing to it, use Signal.peek for one read or Signal.untrack for a larger block.

1let debug = Signal.make(true)
2
3Effect.run(() => {
4 let tracked = Signal.get(count)
5
6 if Signal.peek(debug) {
7 Console.log2("Debug count:", tracked)
8 }
9
10 None
11})

Common Patterns

Common Use Cases

  • Browser APIs: document title, localStorage, media queries, history, and scroll state
  • Timers and subscriptions: intervals, event listeners, sockets, and observers
  • Synchronization: push reactive state into another system
  • Diagnostics: logging, instrumentation, and dev-only inspection

Example: Auto-save

This pattern is common: track a draft, debounce the work, and clean up old timers when the draft changes again.

DraftAutoSave.res
1open Xote
2
3let draft = Signal.make("")
4let saveStatus = Signal.make("Start typing to queue a save")
5
6let handleInput = evt => {
7 let target: {"value": string} = (evt->Obj.magic)["target"]
8 Signal.set(draft, target["value"])
9}
10
11Effect.run(() => {
12 let currentDraft = Signal.get(draft)->String.trim
13
14 if currentDraft == "" {
15 Signal.set(saveStatus, "Start typing to queue a save")
16 None
17 } else {
18 Signal.set(saveStatus, "Saving in 600ms")
19
20 let timeoutId = setTimeout(() => {
21 Console.log2("Saving draft:", currentDraft)
22 }, 500)
23
24 Some(() => clearTimeout(timeoutId))
25 }
26})

Auto-save Draft

Each input change re-runs the effect, resets the timer, and only saves the latest draft after the pause.

StatusStart typing to queue a save
Recent savesCleanup cancels any pending save before the next run.
No saves yet. Type, pause, and the latest draft will be recorded here.
fig. 1 - an effect debounces auto-save work

Effects vs Computed

Ask one question: is the result another value inside the reactive graph, or is it work outside the graph?

  • Use a computed when you are deriving a value from other values
  • Use an effect when you need to talk to something external

Working Style

Best Practices

  • Keep one effect focused on one kind of external work so cleanup stays obvious.
  • Return cleanup whenever you allocate timers, listeners, requests, or subscriptions.
  • Do not use effects to keep derived state in sync. If the output is another value, use a computed.
  • Use peek and untrack deliberately, because they opt out of tracking.

Next Steps