Skip to content

Ecosystem

This page places Ranvier among related libraries in .NET and Fable. Where another library is the better fit, the page says so. Statements about other projects describe their design, not their quality or their user base. See each project's own documentation for current details.

What Ranvier is

Ranvier is a fine-grained reactive core for F# on .NET. Its distinguishing feature is the pending channel: a propagating, recoverable "not ready" status. Straight-line code needs no knowledge of it, and a boundary decides what to show in the meantime. The rest of the model (signals, memos, effects, owners, height-ordered updates and equality cutoff) is well established, and other libraries provide it too.

It is early. The project is pre-release and has one maintainer. It targets net10.0, net8.0 and netstandard2.1, and its public API is designed for F#. Evaluate it with that in mind.

LibraryTrackingUpdate modelAsync state
FSharp.Data.AdaptiveExplicit, through adaptive { } and let!Level-ordered, pullOutside the adaptive model
SignalsDotnetAutomatic, on readPush, built on R3IsComputing flag, cancellation strategies
R3 / System.ReactiveExplicit subscriptionsPush streamsStream events; errors and completion end the stream
Fable.RippleAutomatic, dynamicPull, synchronous settleNot part of the node model
Solid 2.0 (via Partas.Solid)Automatic, dynamicHeight-orderedPending channel and boundaries
RanvierAutomatic, dynamicHeight-ordered, pullPending channel and boundaries

FSharp.Data.Adaptive

A mature F# incremental computation library, used by the Aardvark platform. Its adaptive collections (aset, alist, amap) pass deltas from one stage to the next. Ranvier's projections are keyed collections with filter, map, sortBy, groupBy and fold views. Each view re-reads its upstream keys on a membership or order change. A key reader reports the keys added, removed and replaced, and the new order; readers of value changes and views that apply deltas are still in design. Dependencies are threaded explicitly through the adaptive { } computation expression, which makes them visible in the code. If you need delta-based incremental collections or a proven F# library today, use Adaptive.

SignalsDotnet

A published signals library for .NET MVVM, with integrations listed for WPF, Avalonia, MAUI, Uno, Blazor, Unity and Godot. It tracks automatically and exposes async computations through an IsComputing flag. It is built on R3, so every signal is also an Observable<T>, and every signal implements INotifyPropertyChanged. CollectionSignal wraps an ObservableCollection and reacts both to the collection being replaced and to its contents changing. The SignalsDotnet.Blazor package provides a TrackedScope component that re-renders only the region of markup whose signals changed. SignalsDotnet.Query and SignalsDotnet.AspNetCore, which streams projections as server-sent events, are in alpha. Its cancellation strategies for concurrent async runs are a direct counterpart of Ranvier's flight policies. If you want signals in a C# view model now, it is available and documented.

The two libraries differ mainly in how async state travels: as a flag that each consumer checks, or as a status that propagates to a boundary. In SignalsDotnet, writes wrapped in an atomic operation run each effect once, at the end. Its documentation does not say whether derived values update in height order.

R3 and System.Reactive

Push-based streams, familiar to most .NET developers, with a large operator vocabulary. They model events over time. A derived value that must stay consistent across several inputs is a different shape of problem, and Ranvier addresses that shape. For event pipelines, throttling, windowing and time-based composition, a stream library is the right tool.

Both are active. System.Reactive 7.0.0 was released in July 2026. R3 is a redesign by the author of ReactiveProperty, whose README now says "If you're developing a new application, consider using R3 instead of ReactiveProperty." ReactiveUI 25 runs on its own ReactiveUI.Primitives package (reactiveui/ReactiveUI#4382) and no longer depends on System.Reactive by default. The ReactiveUI.Reactive package keeps System.Reactive interop.

Fable.Ripple

A fine-grained reactive library written from scratch in F# for Fable. It ships a DOM layer, forms, a router and component tests. Its core is written with careful attention to the code Fable generates. It promises that a write settles synchronously, with no scheduler tick. For browser UI in F# without Solid, Ripple provides a complete stack, and Ranvier provides only a core.

Solid (through Partas.Solid)

Ranvier's suspension model follows Solid 2.0's signals: the pending channel, status-parameterised boundaries, re-running bodies from the top, and recovery driven by re-reads. In the browser, Partas.Solid binds Solid's own signals directly. They are what Solid's renderer reads, and they add no F# runtime to the bundle. For a Partas.Solid application, use Solid's signals.

Prior art

Height-ordered, cutoff-aware recomputation descends from Adapton, Jane Street's Incremental and FSharp.Data.Adaptive. The Clef language specification describes a Solid-style reactive surface over incremental nodes, and it leaves asynchronous suspension unspecified.

Diamonds without glitches

A diamond is two derived values that read one source, and a third value that reads both. In Ranvier, one write to a runs d once, and d reads b and c from the same write.

let a = createSignal 1
let b = createMemo (fun _ -> a.Value + 1)
let c = createMemo (fun _ -> a.Value * 10)
let d = createMemo (fun _ -> b.Value + c.Value)
createEffect (fun () -> printfn "d = %d" d.Value)

a.Value <- 2
a.Value <- 3
d = 12
d = 23
d = 34

The same diamond in System.Reactive, with CombineLatest, emits once per leg. The first emission after each write combines the new b with the old c, a state the source never had.

let a = new BehaviorSubject<int> (1)
let b = a.Select (fun x -> x + 1)
let c = a.Select (fun x -> x * 10)
Observable.CombineLatest(b, c, fun b c -> b + c).Subscribe (printfn "d = %d")

a.OnNext 2
a.OnNext 3
d = 12
d = 13
d = 23
d = 24
d = 34

ReactiveUI's multi-property WhenAnyValue behaves the same way: setting A and then B first emits the new A with the old B. The usual workarounds are DelayChangeNotifications () or Throttle (TimeSpan.Zero). Ranvier updates derived values in height order, and d runs after both of its inputs. Two writes that belong together go in one batch.

Owners instead of hooks

Ranvier has no rules of hooks. A node is an object held by reference, and its identity is independent of call order or line number, unlike React hooks or FuncUI's hook identity (FuncUI#212). A body may create nodes inside a branch or a loop, and each run may create a different set. Each node belongs to the owner that was current at its creation, and the next run of that owner disposes it. The cost is that state created in a body starts again on each run: state that has to survive a re-run lives outside the body.

Where Ranvier may fit

These are directions the design is aimed at. The XAML bridge ships in Ranvier.CSharp; the others have no integration yet.

  • XAML view models (WPF, Avalonia, MAUI, Uno, WinUI). ReactiveBindings in Ranvier.CSharp is the INotifyPropertyChanged bridge, usable inside an existing view model. A pending memo maps to a loading property, a failure maps to INotifyDataErrorInfo, and the bindings' IsLoading and HasErrors give one loading and error state covering several sources. The dispatcher follows the UI thread's SynchronizationContext, and each handler runs on the context it subscribed from. No framework-specific package ships yet.
  • Models shared between server and browser. The same F# model code could run on .NET (server rendering, tests, desktop) and through Fable, with suspension that matches Solid 2.0. The Fable target is implemented and not yet published; see Fable (JavaScript) target.
  • Deterministic async in tests. With ManualDispatcher, a test chooses when each flight settles and reads Pending, Ready and Failed states as values, with no UI thread involved. See Testing async state.
  • Avalonia.FuncUI. Its component state already has the shape of a signal.
  • Fluxor stores. For Fluxor users, memos are memoised selectors: a memo over a signal holding the store's state recomputes when that state changes, and an equal result stops at the memo (equality cutoff).
  • Blazor. A boundary maps onto a component. Blazor has no built-in signals, and dotnet/aspnetcore#67329, an open proposal, asks for them. For Blazor Server, ThreadAffinity.Serialised admits the circuit's work one thread at a time; see Blazor Server.

Current gaps

  • No framework-specific UI packages yet. The .NET UI bindings are ReactiveBindings (INotifyPropertyChanged and INotifyDataErrorInfo), ReactiveCommand (ICommand with a derived CanExecute) and Projection.AsObservableCollection.
  • No debounce or throttle, and no flight policy that drops a new run while one is in progress.
  • Projection key readers report membership and order changes only. Readers of row value changes, and views that apply deltas instead of re-reading their upstream keys, are in design.
  • The Fable target is implemented and not yet published; see Fable (JavaScript) target.

The roadmap lists which of these are under consideration.

Choosing

  • Use FSharp.Data.Adaptive for incremental collections, or for a mature F# incremental library.
  • Use SignalsDotnet for signals in C# MVVM today.
  • Use R3 or System.Reactive for event streams and time-based composition.
  • Use Solid through Partas.Solid, or Fable.Ripple, for browser UI in F#.
  • Consider Ranvier when async loading and error states should propagate through derived values to a boundary, and you can work with a pre-release F# library.
Edit this page