EffectSharp 1.1.1

There is a newer version of this package available.
See the version list below for details.
dotnet add package EffectSharp --version 1.1.1
                    
NuGet\Install-Package EffectSharp -Version 1.1.1
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="EffectSharp" Version="1.1.1" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="EffectSharp" Version="1.1.1" />
                    
Directory.Packages.props
<PackageReference Include="EffectSharp" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add EffectSharp --version 1.1.1
                    
#r "nuget: EffectSharp, 1.1.1"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package EffectSharp@1.1.1
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=EffectSharp&version=1.1.1
                    
Install as a Cake Addin
#tool nuget:?package=EffectSharp&version=1.1.1
                    
Install as a Cake Tool

EffectSharp

Lightweight reactive state management for .NET with a Vue 3 inspired API (ref, computed, watch, effect, reactive objects, collections & dictionaries, list diff-binding).

Table of Contents

Overview

EffectSharp brings a fine-grained reactive system to .NET similar to Vue 3's reactivity core. Instead of heavy global state containers, you compose reactive primitives that automatically track dependencies and propagate updates efficiently.

Recent updates include:

  • Deep mode across refs, computed values, proxies, and collections/dictionaries.
  • Reactive dictionaries with per-key dependency tracking.
  • Diff-based binding from lists to ObservableCollection<T>.
  • Unified async batching with TaskManager plus Reactive.NextTick().

It focuses on:

  • Minimal Ceremony: Opt-in reactivity with clear primitives.
  • Predictable Updates: Dependency tracking via transparent property access.
  • Performance: Computed values are lazily evaluated and cached until invalidated.
  • UI Friendly: Batched INotifyPropertyChanged events reduce UI churn (e.g., WPF, MAUI).

Key Features

  • Ref<T> primitive for single mutable reactive values, optional deep mode.
  • Reactive proxies for plain class instances (virtual properties required), deep wrapping via SetDeep().
  • TryCreate helper to wrap if possible, otherwise return original.
  • Dependency-tracked computed values (Computed<T>) with lazy evaluation & caching (supports deep mode).
  • Effects that auto re-run when dependencies change; optional scheduling; Untracked helpers.
  • Flexible watchers: Ref<T>, computed getter functions, tuples and complex shapes, deep tracking.
  • Reactive ObservableCollection<T> enhancement with per-index and list-level dependencies.
  • Reactive dictionaries with per-key tracking and key-set dependency.
  • Diff-based list binding: DiffAndBindTo (keyed or unkeyed) to ObservableCollection<T>.
  • Unified batching via TaskManager; Reactive.NextTick() awaits both effect and notify cycles.

Quick Start

Create a new reactive object, ref, computed value, effect, and watch changes:

using EffectSharp;

// Reactive object (class must be non-sealed; virtual properties needed for proxy)
var product = Reactive.Create(new Product { Name = "Laptop", Price = 1000 });

// Ref primitive (with optional deep mode)
var count = Reactive.Ref(0);
var orderRef = Reactive.Ref(new Order { Product = new Product { Name = "Phone", Price = 500 }, Quantity = 1 }, deep: true);

// Computed value
var priceWithTax = Reactive.Computed(() => product.Price + (int)(product.Price * 0.1));

// Effect (auto reruns when dependencies used inside change)
var effect = Reactive.Effect(() => {
    Console.WriteLine($"Total: {priceWithTax.Value}");
});

product.Price = 1200; // Effect will re-run

// Watch a ref (returns an Effect; dispose to stop)
var subEffect = Reactive.Watch(count, (newVal, oldVal) => {
    Console.WriteLine($"count changed {oldVal} -> {newVal}");
});
count.Value = 1;
await Reactive.NextTick();
subEffect.Dispose();

// Reactive collection
var numbers = Reactive.Collection<int>();
var sum = Reactive.Computed(() => numbers.Sum());
numbers.Add(5); // sum invalidated
Console.WriteLine(sum.Value); // 5

Bind to UI (WPF example)

using CommunityToolkit.Mvvm.Input;
using EffectSharp;

// ViewModel with reactive properties
public class MyViewModel
{
    public Ref<int> Counter { get; } = Reactive.Ref(0);
    public Computed<string> DisplayText { get; }

    public MyViewModel()
    {
        DisplayText = Reactive.Computed(() => $"Counter: {Counter.Value}");
    }
    
    [RelayCommand]
    public void Increment() => Counter.Value++;

    [RelayCommand]
    public void Decrement() => Counter.Value--;
}

<Button Command="{Binding IncrementCommand}" Content="+" />
<TextBlock Text="{Binding DisplayText.Value}" />
<Button Command="{Binding DecrementCommand}" Content="-" />

Core Concepts

Ref<T>

A lightweight holder for a single reactive value. Accessing Value from inside effects/computed getters tracks the dependency; assigning new values triggers subscribed effects.

var counter = Reactive.Ref(0);
Reactive.Effect(() => Console.WriteLine(counter.Value));
counter.Value++; // Effect prints updated value

Reactive Objects (Reactive.Create / CreateDeep / TryCreate)

Wrap an existing instance in a dynamic proxy using Castle.DynamicProxy. Requirements:

  • Class must be non-sealed.
  • Properties you want tracked must be virtual. CreateDeep additionally enables deep behavior via SetDeep() internally. TryCreate returns the original instance if it cannot be proxied.
var order = Reactive.CreateDeep(new Order {
    Product = new Product { Name = "Phone", Price = 500 },
    Quantity = 2
});
Reactive.Effect(() => Console.WriteLine(order.Product.Price));
order.Product.Price = 600; // Effect reruns

Computed Values (Reactive.Computed)

Lazy, cached derivations. Recomputed only when one of the dependencies accessed during its last evaluation changes.

var product = Reactive.Create(new Product { Name = "Tablet", Price = 300 });
var priceWithTax = Reactive.Computed(() => product.Price + (int)(product.Price * 0.1));
Console.WriteLine(priceWithTax.Value); // 330
product.Price = 400;
Console.WriteLine(priceWithTax.Value); // 440 (recomputed)

Effects (Reactive.Effect)

Encapsulate reactive logic. Any dependency read during its execution registers it as a subscriber. Supports custom scheduler for throttling/coalescing and Untracked helpers to perform side effects without capturing dependencies.

var throttled = Reactive.Effect(() => {
    _ = product.Price; // Track dependency
}, effect => {
    // Simple scheduler example: defer with Task.Run
    Task.Run(() => effect.Execute());
});

Watching Changes (Reactive.Watch)

Watch lets you observe specific sources without executing a full effect body manually. It returns an Effect you can dispose to stop watching. Variants:

  • Ref-based: Watch(refObj, (newVal, oldVal) => ...)
  • Getter-based: Watch(() => (refA.Value, refB.Value), callback); supports tuples and enumerable shapes
  • Options: Immediate, Deep, EqualityComparer<T>
var refA = Reactive.Ref(1);
var refB = Reactive.Ref(10);
var watcher = Reactive.Watch(() => (refA.Value, refB.Value), (_, _) => Console.WriteLine("Either changed"));
refA.Value = 2; // triggers
refB.Value = 20; // triggers
await Reactive.NextTick();
watcher.Dispose();

Reactive Collections (Reactive.Collection<T>) and Dictionaries (Reactive.Dictionary<TKey, TValue>)

ReactiveCollection<T> extends ObservableCollection<T> with tracked dependencies for per-index access and overall list changes.

var list = Reactive.Collection([10, 20, 30]);
var first = Reactive.Computed(() => list[0]);
list[0] = 100; // invalidates first
Console.WriteLine(first.Value); // 100

ReactiveDictionary<TKey, TValue> implements IDictionary<TKey, TValue> with tracked dependencies for individual keys and the overall key set.

var dict = Reactive.Dictionary<string, int>();
var hasFoo = Reactive.Computed(() => dict.ContainsKey("foo"));
Console.WriteLine(hasFoo.Value); // false
dict["foo"] = 2; // invalidates hasFoo via key-set dependency
await Reactive.NextTick();
Console.WriteLine(hasFoo.Value); // true

Usage Examples

See a-littlebit/EffectSharp · GitHub for more usage examples.

Advanced Topics

Notification Batching

TaskManager batches both effect triggers and UI notifications. Configure intervals/schedulers as needed:

TaskManager.EffectIntervalMs = 0; // process effects ASAP
TaskManager.NotifyIntervalMs = 16; // UI-friendly batching
TaskManager.FlushNotifyAfterEffectBatch = true; // auto flush after effect batch

Use await Reactive.NextTick() to await both effect and notify ticks.

Custom Effect Schedulers

Provide a scheduler to delay or merge effect executions:

var effect = Reactive.Effect(() => DoWork(), eff => {
    // simple debounce
    Timer? timer = null;
    timer?.Dispose();
    timer = new Timer(_ => eff.Execute(), null, 50, Timeout.Infinite);
});

Deep Watching

WatchOptions.Deep = true tracks nested reactive properties (including refs/collections/dictionaries). Use the NonReactive attribute to opt out for specific properties/classes.

Comparison with Vue 3

Vue 3 Concept EffectSharp Equivalent
ref() Reactive.Ref()
reactive() Reactive.Create() / Reactive.CreateDeep()
computed() Reactive.Computed()
watch() Reactive.Watch()
effect (internal) Reactive.Effect()
Track dependencies Dependency.Track() / Property interception
nextTick() Reactive.NextTick()
Microtask queue TaskManager configuration

Limitations

  • Only non-sealed classes with virtual properties are proxied.
  • Structs / records (non-class) are not reactive directly (wrap them inside a Ref<T>).
  • Diff-binding provided for lists to ObservableCollection<T>.
  • Deep watch may cause overhead on large graphs.
  • No persistence layer integration yet.
  • Use NonReactive to exclude properties/classes from tracking.

FAQ

Q: Do I need to adjust TaskManager Configuration?
Usually no, sensible defaults are provided. Adjust only if you need custom batching behavior, or your application requires specific threading contexts (e.g., specific UI thread).

Q: How do I stop tracking?
Dispose the Effect returned by Reactive.Effect() or Reactive.Watch().
If you want to perform side effects without tracking dependencies, use Effect.Untracked(() => { ... }).

Q: Is EffectSharp thread-safe?
Reactive proxies are thread-safe or not depending on the underlying object except for deep ones, which are not thread-safe until SetDeep() is done.
Reactive primitives like Ref<T>, Computed<T> are not guaranteed to be thread-safe. If necessary, you can implement your own thread-safe reactive types for specific underlying data structures using the core dependency tracking system. Effects and watchers are scheduled via TaskManager and each runs with a lock to prevent concurrent execution, which means effects/watchers themselves are thread-safe.
Note that the UI notification scheduler in TaskManager should be set to the UI thread context when used in UI applications. The default is TaskScheduler.FromCurrentSynchronizationContext().

Q: What is the difference between keyed and unkeyed diff-binding?
Keyed binding is preferred when items have a unique identifier property, it is usually more efficient.

License

This project is licensed under the MIT License - see the LICENSE file for details.

Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 was computed.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 was computed.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
.NET Core netcoreapp2.0 was computed.  netcoreapp2.1 was computed.  netcoreapp2.2 was computed.  netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.0 is compatible.  netstandard2.1 was computed. 
.NET Framework net461 was computed.  net462 was computed.  net463 was computed.  net47 was computed.  net471 was computed.  net472 was computed.  net48 was computed.  net481 was computed. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen40 was computed.  tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.4.0 86 1/4/2026
1.3.2 178 12/25/2025
1.3.1 172 12/22/2025
1.3.0 156 12/21/2025
1.2.2 216 12/14/2025
1.2.1 136 12/12/2025
1.2.0 440 12/10/2025
1.1.2 654 12/3/2025
1.1.1 581 12/1/2025
1.1.0 130 11/29/2025
1.0.0 335 11/21/2025