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
@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>
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 = "";
}
}
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>
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
Component Update Call
User interaction triggers Update(s => s.Increment())
Store Transformation
Store applies the updater function in a thread-safe manner
Immutable Update
New state instance created, original remains unchanged
Notification
All subscribed components receive state change notification
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.
Components using selectors can see up to 25x fewer re-renders in high-frequency update scenarios.
Basic Example
@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
When your state has many properties and components only need a few.
When state updates frequently but most components don't care about every change.
When you've profiled and identified unnecessary re-renders as a bottleneck.
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:
@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());
}
}
}