SkillAgentSearch skills...

EasyAppDev.Blazor.Store

Zustand-inspired state management for Blazor (WebAssembly/Server/MAUI). Query system, cross-tab sync, SignalR collaboration, undo/redo, optimistic updates, Redux DevTools. .NET 8/9/10.

Install / Use

/learn @mashrulhaque/EasyAppDev.Blazor.Store

README

EasyAppDev.Blazor.Store - State Management for Blazor

A Zustand-inspired state management library for Blazor WebAssembly, Blazor Server, and MAUI Hybrid. Zero boilerplate. Built-in data fetching with caching. Cross-tab sync. Real-time collaboration via SignalR. Undo/redo history. Redux DevTools support. All type-safe with C# records.

NuGet version for EasyAppDev.Blazor.Store state management library License: MIT - Free to use for Blazor state management

<div align="center"> <a href="https://mashrulhaque.github.io/EasyAppDev.Blazor.Store/"> <img src="https://img.shields.io/badge/📚_Full_Documentation-4A90E2?style=for-the-badge" alt="Full documentation for EasyAppDev.Blazor.Store - Blazor state management library" /> </a> <a href="http://blazorstore.easyappdev.com/"> <img src="https://img.shields.io/badge/🚀_Live_Demo-28A745?style=for-the-badge" alt="Live demo of Blazor state management with cross-tab sync and undo-redo" /> </a> </div>
// Define state as a C# record
public record CounterState(int Count)
{
    public CounterState Increment() => this with { Count = Count + 1 };
}
@inherits StoreComponent<CounterState>

<h1>@State.Count</h1>
<button @onclick="@(() => UpdateAsync(s => s.Increment()))">+</button>

That's it. No actions, no reducers, no dispatchers. State updates propagate to all subscribers automatically.

Upgrading from v1.x? See Breaking Changes in v2.0.0 for migration guide.


Why This Library?

| Problem | Solution | |---------|----------| | Boilerplate everywhere (actions, reducers, dispatchers) | State = C# record with methods | | 10 components fetch same data = 10 API calls | Request deduplication - one fetch, shared result | | Tab 1 updates cart, Tab 2 shows stale data | Cross-tab sync via BroadcastChannel | | No undo for user mistakes | Built-in undo/redo with memory limits | | Loading/error state spaghetti | AsyncData<T> replaces boolean flags | | Optimistic UI rollback is painful | Built-in rollback on server error |

.NET 8, 9, 10 · Blazor Server, WebAssembly, Auto, MAUI Hybrid


Quick Start

Installation

.NET CLI

dotnet add package EasyAppDev.Blazor.Store

Package Manager Console

Install-Package EasyAppDev.Blazor.Store

PackageReference (add to your .csproj)

<PackageReference Include="EasyAppDev.Blazor.Store" Version="2.0.*" />

Setup

// Program.cs
builder.Services.AddStoreWithUtilities(
    new CounterState(0),
    (store, sp) => store.WithDefaults(sp, "Counter"));
@page "/counter"
@inherits StoreComponent<CounterState>

<h1>@State.Count</h1>
<button @onclick="@(() => UpdateAsync(s => s.Increment()))">+</button>
<button @onclick="@(() => UpdateAsync(s => s.Decrement()))">-</button>

That's it. All components subscribed to CounterState update automatically.


Table of Contents

Getting Started

Data Fetching

Sync & Collaboration

History & Advanced

Reference


Core Concepts - Immutable State with C# Records

State = Immutable Record

public record TodoState(ImmutableList<Todo> Todos)
{
    public static TodoState Initial => new(ImmutableList<Todo>.Empty);

    // Pure transformation methods - no side effects
    public TodoState AddTodo(string text) =>
        this with { Todos = Todos.Add(new Todo(Guid.NewGuid(), text, false)) };

    public TodoState ToggleTodo(Guid id) =>
        this with {
            Todos = Todos.Select(t =>
                t.Id == id ? t with { Completed = !t.Completed } : t
            ).ToImmutableList()
        };

    public TodoState RemoveTodo(Guid id) =>
        this with { Todos = Todos.RemoveAll(t => t.Id == id) };

    // Computed properties
    public int CompletedCount => Todos.Count(t => t.Completed);
}

Component = StoreComponent<T>

@page "/todos"
@inherits StoreComponent<TodoState>

<input @bind="newTodo" @onkeyup="HandleKeyUp" />

@foreach (var todo in State.Todos)
{
    <div>
        <input type="checkbox" checked="@todo.Completed"
               @onchange="@(() => UpdateAsync(s => s.ToggleTodo(todo.Id)))" />
        @todo.Text
        <button @onclick="@(() => UpdateAsync(s => s.RemoveTodo(todo.Id)))">X</button>
    </div>
}

<p>Completed: @State.CompletedCount / @State.Todos.Count</p>

@code {
    string newTodo = "";

    async Task HandleKeyUp(KeyboardEventArgs e)
    {
        if (e.Key == "Enter" && !string.IsNullOrWhiteSpace(newTodo))
        {
            await UpdateAsync(s => s.AddTodo(newTodo));
            newTodo = "";
        }
    }
}

Interface Segregation

The IStore<T> interface composes three focused interfaces:

// Read-only state access
public interface IStateReader<TState> where TState : notnull
{
    TState GetState();
}

// State update operations
public interface IStateWriter<TState> where TState : notnull
{
    Task UpdateAsync(Func<TState, TState> updater, string? action = null);
    Task UpdateAsync(Func<TState, Task<TState>> asyncUpdater, string? action = null);
}

// Subscription management
public interface IStateObservable<TState> where TState : notnull
{
    IDisposable Subscribe(Action<TState> callback);
    IDisposable Subscribe<TSelected>(Func<TState, TSelected> selector, Action<TSelected> callback);
}

// Full store interface
public interface IStore<TState> :
    IStateReader<TState>,
    IStateWriter<TState>,
    IStateObservable<TState>,
    IDisposable
    where TState : notnull
{
}

Registration Options

AddSecureStore (Recommended)

Automatic security configuration based on environment:

// Simplest secure registration
builder.Services.AddSecureStore(
    new AppState(),
    "App",
    opts =>
    {
        opts.PersistenceKey = "app-state";   // LocalStorage
        opts.EnableTabSync = true;            // Cross-tab sync
        opts.EnableHistory = true;            // Undo/redo
    });

Security profiles applied automatically:

  • Development: DevTools enabled, permissive validation
  • Production: No DevTools, message signing enabled, validation warnings
  • Strict: All Production features + throws on any security warning

AddStoreWithUtilities

Standard registration with all utilities:

builder.Services.AddStoreWithUtilities(
    TodoState.Initial,
    (store, sp) => store
        .WithDefaults(sp, "Todos")           // DevTools + Logging
        .WithPersistence(sp, "todos"));      // LocalStorage

AddScopedStoreWithUtilities

Scoped store for Blazor Server per-user isolation (required for persistence/DevTools/TabSync):

builder.Services.AddScopedStoreWithUtilities(
    new UserSessionState(),
    (store, sp) => store
        .WithDefaults(sp, "Session")
        .WithPersistence(sp, "session-state"));  // Works with scoped stores

Note: WithPersistence(sp, key) requires scoped store registration in Blazor Server. Using it with singleton stores (AddStore) will throw an InvalidOperationException with guidance to use scoped stores.

AddStore / AddScopedStore

Minimal registration without utilities:

// Singleton
builder.Services.AddStore(new CounterState(0));

// Scoped
builder.Services.AddScopedStore(new CounterState(0));

// With factory
builder.Services.AddStore(
    sp => new AppState(sp.GetRequiredService<IConfiguration>()),
    (store, sp) => store.WithLogging());

AddStoreWithHistory

Store with undo/redo support:

builder.Services.AddStoreWithHistory(
    new EditorState(),
    opts => opts
        .WithMaxSize(100)
        .WithMaxMemoryMB(50)
        .ExcludeActions("CURSOR_MOVE", "SELECTION"),
    (store, sp) => store.WithDefaults(sp, "Editor")
);

Async Helpers

Built-in utilities for common async patterns. No more writing the same loading/error handling.

ExecuteCachedAsync - Request Deduplication

The problem: 10 components load the same user. Result: 10 API calls, 20 state updates.

The solution: ExecuteCachedAsync deduplicates both the fetch AND the state updates:

// 10 components call this concurrently → 1 API call, 2 state updates
async Task LoadProduct(int productId, CancellationToken ct = default)
{
    await ExecuteCachedAsync(
        $"product-{productId}",
        async () => await ProductService.GetAsync(productId),
        loading: s => s with { Product = s.Product.ToLoading() },
        success: (s, product) => s with { Product = AsyncData.Success(product) },
        error: (s, ex) => s with { Product = AsyncData.Failure(ex.Message) },
        cacheFor: TimeSpan.FromMinutes(5),
        cancellationToken: ct
    );
}

| Scenario | Method | |----------|--------| | Multiple components load same data | ExecuteCachedAsync |

View on GitHub
GitHub Stars57
CategoryDevelopment
Updated5d ago
Forks4

Languages

C#

Security Score

100/100

Audited on Mar 26, 2026

No findings