Effects
createEffect body runs the body and tracks what it reads. When a tracked value changes, the effect runs again.
Use effects to connect reactive state to external work, such as logging, subscriptions or UI updates. Use a memo when you need a derived value that other computations can read.
Outside a batch or an existing flush, the first run is immediate, and effects triggered by a write run before that write returns. Inside a batch, effects wait until the batch ends.
Test your understanding
The first effect writes fahrenheit; the second reads it. In what order are the messages logged when celsius changes to 100? Does the Fahrenheit effect run before the write returns?
let effectLog =
use graph = new Graph ()
use _ = graph.Activate ()
let log = ResizeArray ()
let celsius = createSignal 0
let fahrenheit = createSignal 32
createEffect (fun () ->
log.Add $"celsius effect: {celsius.Value}"
fahrenheit.Value <- celsius.Value * 9 / 5 + 32)
createEffect (fun () -> log.Add $"fahrenheit effect: {fahrenheit.Value}")
log.Add "-- write celsius <- 100"
celsius.Value <- 100
log.Add "-- write returned"
List.ofSeq log
effectLog |> List.iter (printfn "%s")
Answer
celsius effect: 0
fahrenheit effect: 32
-- write celsius <- 100
celsius effect: 100
fahrenheit effect: 212
-- write returned
The Celsius effect writes fahrenheit, which schedules the Fahrenheit effect in the same flush. Both run before the outer write returns.
Creating or writing from inside a flush
An effect created inside a batch or a running flush first runs when that batch or flush reaches it. A write inside an effect joins the flush already running; the effects it wakes run before the outer write returns.
createEffect returns unit. Its lifetime is managed by the enclosing scope; see Scopes and disposal.
Test your understanding
How many times does this effect run? Does the write after disposal run it again?
let handleRuns =
use graph = new Graph ()
let count = Signal (graph, 0)
let effect = new Effect (graph, fun () -> count.Value |> ignore)
count.Value <- 1
effect.Dispose ()
count.Value <- 2
effect.Runs
printfn "runs: %d" handleRuns
Answer
runs: 2
It runs on construction and on the first write. Disposal stops it responding to later writes.
Changes made during a flush
- An effect that disposes itself, or disposes an effect queued after it, stops that effect running.
- An effect that writes a signal it reads re-runs until the value stops changing.
flush ()called from inside an effect body joins the flush already running.
let convergedAt, convergeRuns =
use graph = new Graph ()
use _ = graph.Activate ()
let count = createSignal 0
let mutable runs = 0
createEffect (fun () ->
runs <- runs + 1
if count.Value < 3 then count.Value <- count.Value + 1)
count.Value, runs
printfn "converged at %d after %d runs" convergedAt convergeRuns
converged at 3 after 4 runs
Failures
Test your understanding
When the first effect throws, does the write return? Does the second effect run? Where can you find the exception?
let writeReturned, effectError, laterEffectRan =
use graph = new Graph ()
let count = Signal (graph, 0)
let failing =
new Effect (graph, fun () -> if count.Value > 0 then failwith "boom")
let mutable laterEffectRan = false
new Effect (graph, fun () -> if count.Value > 0 then laterEffectRan <- true) |> ignore
count.Value <- 1
true, failing.Error.Message, laterEffectRan
printfn "write returned: %b, Effect.Error: %s, later effect ran: %b" writeReturned effectError laterEffectRan
Answer
write returned: true, Effect.Error: boom, later effect ran: true
The write returns and the later effect runs. The failing effect keeps the exception in its Error property.