Skip to content

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.

Example code
let count = createSignal 0
let mutable messages = []
createEffect (fun () ->
    let value = count.Value
    messages <- messages @ [value]
    printfn "count = %d" value)

controls [
    button "Write 1" (fun () -> count.Value <- 1)
    |> describe "The effect runs synchronously for the changed value."
    |> expect "The effect sees its initial value and the write." (fun () -> messages = [0; 1])
    button "Write 1 again" (fun () -> count.Value <- 1)
    |> describe "The equal write does not run the effect."
    |> expect "The equal write adds no message." (fun () -> messages = [0; 1])
]
Hover a node for its state; click it for why it last ran.

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 Roots and owners.

createEffectNode body creates the same effect as createEffect on the active graph, owned by the enclosing scope, and returns it as an INode. Use it when code that creates effects through the ambient graph needs to read an effect's Status, for example to tell whether its last run is pending, failed or stale:

let price = createSignal 2
let effect = createEffectNode (fun () -> printfn "price = %d" price.Value)

batch (fun () ->
    price.Value <- 5
    effect.Status.HasFlag Status.Stale) // true: the effect re-runs when the batch ends
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

Binding a source to a target

bindSignal source target write and bindMemo source target write connect one signal or memo to one target. write receives the target and the source's value. Use a binding instead of an effect when the only job is to copy a single source into a target, such as a node property in a view. Use an effect when the work reads several sources or depends on what it reads.

type Label () =
    member val Text = "" with get, set

let label = Label ()
let name = createSignal "Ada"
bindSignal name label (fun target value -> target.Text <- value)

The binding writes "Ada" on the next flush and follows changes to name. Reads inside write are untracked; the callback must not create nodes or register cleanup. A pending or failed memo leaves the target unchanged. The current owner disposes the binding.

See Fixed-dependency bindings for flush timing, equality, writer failures and lifetime, and the binding benchmarks for measurements and limitations.

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.

Performance

Instruction counts for effect work, MVU dispatch and editables, compared with other libraries and followed across commits, are on Benchmarks → Effects.

Edit this page