Skip to content
Preview APIs may change.

ranvier

Fine-grained reactive computation for .NET.

Signals, memos and effects where loading and failure are part of the graph. An async value's pending and failed states reach every value derived from it and stop at a boundary, so no view model tracks IsBusy by hand. Glitch-free, owned and inspired by Solid.
A traced build lets people and agents ask why anything ran; a release build compiles the tracing out.

using var graph = new Graph();
using var _ = graph.Activate();

var price = AsyncSource<int>();
var total = Memo(() => price.Value * 3);
var view = Boundary(
    () => $"Total {total.Value}",
    () => "Loading…",
    ex => $"Unavailable: {ex.Message}");

// view: Fallback
price.Settle(4);
price.Fail(new Exception("feed offline"));
price.Settle(5);
view.TryValue
  1. FallbackReady "Loading…"IsWaiting = true
  2. ReadyReady "Total 12"IsWaiting = false
  3. RecoveredReady "Unavailable: feed offline"Caught ≠ null
  4. ReadyReady "Total 15"Caught = null
Output from running this code against Ranvier ad2d84d.

States you can name

Every reader observes one of six states. The docs, the marks and the API use the same names. Hover a state to hold the map in it.

Watch the graph think

A traced build records every write, mark, run and flight, with the source line that caused it. This is the example from the top of the page, running on the real engine compiled to JavaScript with tracing on. Press a button and follow the event along the edges; hover a node for its state, click it for why it last ran. How to read a map.

let price = createAsyncSource<int> ()
let total = createMemo (fun _ -> price.Value * 3)

let view =
    createBoundary
        (fun _ -> "Loading…")
        (fun ex _ -> "Unavailable: " + ex.Message)
        (fun () -> sprintf "Total %d" total.Value)

createEffect (fun () -> printfn "%s" view.Value)

let offline = exn "feed offline"

controls [
    button "Settle 4" (fun () -> price.Settle 4)
    button "Fail" (fun () -> price.Fail offline)
    button "Settle 5" (fun () -> price.Settle 5)
]

Edit this map in your browser

Why Ranvier

The parts of reactive state that .NET developers most often rebuild by hand, built into the library.

Loading and errors, derived

Pending and failure travel on their own channel. They pass from an async source through every memo that reads it to the nearest boundary, which shows a fallback or a recovered value. A flight policy decides what happens to superseded work: cancel it, keep only the latest, queue it, or finish it and run once more. During a refresh Peek keeps the last value.

Errors are state, not the end of the stream

A failure is a value that readers see and a boundary recovers from. The next successful run clears it, and every dependency stays live. ErrorOrigin names the node the failure started in.

Owners instead of leaks

Every memo, effect and projection row belongs to an owner. Disposing the owner disposes them, in a fixed order. Lifetimes are deterministic and never depend on the garbage collector or on a forgotten unsubscribe.

Glitch-free diamonds

A value that reads two paths from one source runs once per write, and both paths it reads come from that write. It never sees one path updated and the other stale.

Built for C# and XAML

ReactiveBindings raises PropertyChanged for derived properties with no dependency attributes. ReactiveCommand derives CanExecute and IsRunning from the graph. C# callers need no F# option types.

Incremental collections

Keyed projections give each row its own reactive value. A key reader reports only the keys added, removed or replaced since its last read. A bound ObservableCollection receives individual adds, removes and moves instead of a reset.

A written threading contract

A graph checks that it is called from its own thread, and the contract lists which calls may come from other threads. Serialised mode accepts a Blazor Server circuit's changing threads and raises on genuine concurrency.

Deterministic async tests

ManualDispatcher and Settle let a test decide when each flight lands. Loading, failure and cancellation are tested with no timers, sleeps or polling.

Portable, with no platform package

One core for net10.0, net8.0 and netstandard2.1, with no UI-framework dependency. The untraced build is Native AOT and trim clean, and the library compiles to JavaScript with Fable.

Adopt it one view at a time

Ranvier.Elmish keeps an existing init and update and reads the model through selector memos. Editable values cover forms seeded from upstream data.