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 Xote23let count = Signal.make(0)45Effect.run(() => {6 Console.log2("Count:", Signal.get(count))7 None8})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)45Effect.run(() => {6 if Signal.get(showDetails) {7 Console.log2("Name:", Signal.get(name))8 Console.log2("Age:", Signal.get(age))9 }10 None11})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")23Effect.run(() => {4 let currentUrl = Signal.get(url)5 let controller = AbortController.make()67 fetch(currentUrl, {"signal": controller##signal})->ignore89 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 None4})56disposer.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)23Effect.run(() => {4 let tracked = Signal.get(count)56 if Signal.peek(debug) {7 Console.log2("Debug count:", tracked)8 }910 None11})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.
1open Xote23let draft = Signal.make("")4let saveStatus = Signal.make("Start typing to queue a save")56let handleInput = evt => {7 let target: {"value": string} = (evt->Obj.magic)["target"]8 Signal.set(draft, target["value"])9}1011Effect.run(() => {12 let currentDraft = Signal.get(draft)->String.trim1314 if currentDraft == "" {15 Signal.set(saveStatus, "Start typing to queue a save")16 None17 } else {18 Signal.set(saveStatus, "Saving in 600ms")1920 let timeoutId = setTimeout(() => {21 Console.log2("Saving draft:", currentDraft)22 }, 500)2324 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.
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
peekanduntrackdeliberately, because they opt out of tracking.
Next Steps
- Move to Components to see how effects fit into real UI code.
- Read Batching when several writes should flush as one coordinated update.