Components

Blazor components with automatic reactivity and subscription management

Overview

EasyAppDev.Blazor.Store provides two base components for integrating state management into your Blazor applications. These components handle subscription management, automatic updates, and provide convenient methods for state mutations.

StoreComponent<TState>

The primary component for state management. Inherit from StoreComponent<TState> to get automatic subscription to state changes and reactive updates.

What You Get

๐Ÿ”„

Automatic Subscription

Components automatically subscribe to state changes on initialization.

๐Ÿ’พ

State Access

Direct access to current state via the State property.

โœจ

Update Methods

Convenient Update() methods for state transformations.

๐Ÿงน

Automatic Cleanup

Subscriptions are automatically disposed when component is disposed.

Basic Usage

TodoList.razor
@page "/todos"
@inherits StoreComponent<TodoState>

<h1>My Todo List</h1>

@foreach (var todo in State.Todos)
{
    <div>
        <input type="checkbox"
               checked="@todo.Completed"
               @onchange="@(() => Update(s => s.ToggleTodo(todo.Id)))" />
        <span>@todo.Text</span>
        <button @onclick="@(() => Update(s => s.RemoveTodo(todo.Id)))">๐Ÿ—‘๏ธ</button>
    </div>
}

@code {
    async Task AddTodo() => await Update(s => s.AddTodo("New task"));
}

Component Patterns

There are three main patterns for updating state from components, each suited for different scenarios.

Pattern 1: Inline Updates (Simple)

For simple, direct state updates without additional logic:

<button @onclick="@(() => Update(s => s.Increment()))">+</button>
<button @onclick="@(() => Update(s => s.Decrement()))">-</button>
<button @onclick="@(() => Update(s => s.Reset()))">Reset</button>
Best for:

Simple state transformations that don't require validation or additional logic.

Pattern 2: Component Methods (With Validation/Logic)

When you need to add validation, conditional logic, or side effects:

<button @onclick="IncrementIfValid">+</button>

@code {
    void IncrementIfValid()
    {
        if (State.Count < 100)
        {
            Update(s => s.Increment());
        }
        else
        {
            ShowError("Cannot exceed 100");
        }
    }

    async Task AddTodo()
    {
        if (string.IsNullOrWhiteSpace(newTodo))
            return;

        Logger.LogInformation("Adding todo: {Text}", newTodo);
        await Update(s => s.AddTodo(newTodo));
        newTodo = "";
    }
}
Best for:

Updates that require validation, logging, or conditional execution.

Pattern 3: Ad-hoc Inline (Flexibility)

For one-off updates or dynamic state changes:

<input type="number"
       @bind="newValue"
       @bind:after="@(() => Update(s => s with { Count = newValue }))" />

<button @onclick="@(() => Update(s => s with { Count = newValue, Name = newName }))">
    Set Values
</button>
Best for:

Quick inline transformations or when you don't have a dedicated state method.

Update Flow

Understanding how state updates flow through your application:

Component calls Update(s => s.Method())
    โ†“
Store applies transformation
    โ†“
New state created (old unchanged)
    โ†“
All subscribers notified
    โ†“
Components re-render automatically
1

Component Update Call

User interaction triggers Update(s => s.Increment())

2

Store Transformation

Store applies the updater function in a thread-safe manner

3

Immutable Update

New state instance created, original remains unchanged

4

Notification

All subscribed components receive state change notification

5

Re-render

Components call StateHasChanged() and re-render with new state

SelectorStoreComponent<TState>

For performance optimization, use SelectorStoreComponent to subscribe to only specific parts of your state. This dramatically reduces unnecessary re-renders in large applications.

Performance Boost

Components using selectors can see up to 25x fewer re-renders in high-frequency update scenarios.

Basic Example

CounterDisplay.razor
@page "/counter-display"
@inherits SelectorStoreComponent<AppState>

<!-- Only re-renders when Counter changes, not when other state properties change -->
<h1>Count: @State.Counter</h1>

@code {
    // Component only re-renders when Counter changes
    protected override object SelectState(AppState state) => state.Counter;
}

Selector Patterns

Single Property

protected override object SelectState(AppState s) => s.UserName;

Multiple Properties (Tuple)

protected override object SelectState(AppState s) => (s.UserName, s.IsLoading);

Computed Values (Record)

protected override object SelectState(TodoState s) => new TodoStats(
    Total: s.Todos.Count,
    Completed: s.Todos.Count(t => t.Completed)
);

Filtered Collections

protected override object SelectState(TodoState s) =>
    s.Todos.Where(t => !t.Completed).ToImmutableList();

When to Use Selectors

๐Ÿ“Š Large State Objects

When your state has many properties and components only need a few.

โšก High-Frequency Updates

When state updates frequently but most components don't care about every change.

๐ŸŽฏ Performance Critical

When you've profiled and identified unnecessary re-renders as a bottleneck.

๐Ÿ“‹ List Components

When rendering large lists where each item should only update when its data changes.

API Reference

StoreComponent<TState>

public abstract class StoreComponent<TState> : ComponentBase
{
    // State access
    protected TState State { get; }

    // Core updates
    protected Task Update(Func<TState, TState> updater, string? action = null);
    protected Task UpdateAsync(Func<TState, Task<TState>> asyncUpdater, string? action = null);

    // Async helpers (when utilities are registered)
    protected Task UpdateDebounced(Func<TState, TState> updater, int delayMs, string? action = null);
    protected Task UpdateThrottled(Func<TState, TState> updater, int intervalMs, string? action = null);
    protected Task ExecuteAsync<T>(
        Func<Task<T>> action,
        Func<TState, TState> loading,
        Func<TState, T, TState> success,
        Func<TState, Exception, TState>? error = null);
    protected Task<T> LazyLoad<T>(string key, Func<Task<T>> loader, TimeSpan? cacheFor = null);
}

SelectorStoreComponent<TState>

public abstract class SelectorStoreComponent<TState> : ComponentBase
{
    // State access
    protected TState State { get; }
    protected object? Selected { get; }

    // Required: Override to select specific state slice
    protected abstract object SelectState(TState state);

    // Update methods (no async helpers)
    protected Task Update(Func<TState, TState> updater, string? action = null);
}

Complete Example

A real-world example combining all component patterns:

TodoPage.razor
@page "/todos"
@inherits StoreComponent<TodoState>

<div class="todo-app">
    <h1>Todo List</h1>

    <!-- Pattern 1: Inline updates -->
    <input @bind="newTodo"
           @onkeyup="HandleKeyUp"
           placeholder="What needs to be done?" />

    <!-- Stats using computed values -->
    <div class="stats">
        <span>Total: @State.Todos.Count</span>
        <span>Completed: @State.CompletedCount</span>
        <span>Progress: @State.CompletionRate.ToString("F1")%</span>
    </div>

    <!-- Todo list -->
    @foreach (var todo in State.Todos)
    {
        <div class="todo-item">
            <input type="checkbox"
                   checked="@todo.Completed"
                   @onchange="@(() => Update(s => s.ToggleTodo(todo.Id)))" />
            <span class="@(todo.Completed ? "completed" : "")">@todo.Text</span>
            <button @onclick="@(() => Update(s => s.RemoveTodo(todo.Id)))">๐Ÿ—‘๏ธ</button>
        </div>
    }
</div>

@code {
    string newTodo = "";

    // Pattern 2: Component method with validation
    async Task HandleKeyUp(KeyboardEventArgs e)
    {
        if (e.Key == "Enter" && !string.IsNullOrWhiteSpace(newTodo))
        {
            await Update(s => s.AddTodo(newTodo));
            newTodo = "";
        }
    }

    // Pattern 2: Component method with logging
    async Task ClearCompleted()
    {
        var count = State.Todos.Count(t => t.Completed);
        if (count > 0)
        {
            Logger.LogInformation("Clearing {Count} completed todos", count);
            await Update(s => s.ClearCompleted());
        }
    }
}

Next Steps