Real-World Examples
Complete examples demonstrating common patterns and use cases with EasyAppDev.Blazor.Store.
Todo List with Persistence
A complete todo application with LocalStorage persistence, demonstrating immutable list operations and automatic state saving.
1
Define State
TodoState.cs
public record Todo(Guid Id, string Text, bool Completed);
public record TodoState(ImmutableList<Todo> Todos)
{
public static TodoState Initial => new(ImmutableList<Todo>.Empty);
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) };
}
2
Register Store
Program.cs
builder.Services.AddStoreWithUtilities(
TodoState.Initial,
(store, sp) => store
.WithDefaults(sp, "Todos")
.WithPersistence(sp, "todos")); // Auto-save to LocalStorage
3
Create Component
TodoList.razor
@page "/todos"
@inherits StoreComponent<TodoState>
<input @bind="newTodo" @onkeyup="HandleKeyUp" placeholder="What needs to be done?" />
@foreach (var todo in State.Todos)
{
<div>
<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>
}
@code {
string newTodo = "";
async Task HandleKeyUp(KeyboardEventArgs e)
{
if (e.Key == "Enter" && !string.IsNullOrWhiteSpace(newTodo))
{
await Update(s => s.AddTodo(newTodo));
newTodo = "";
}
}
}
Key Features
- Immutable Collections: Uses
ImmutableList<T>for safe list operations - Persistence: Automatically saves to LocalStorage with
WithPersistence() - Pure Functions: All state methods return new state instances
Authentication with Async Helpers
Complete authentication flow using AsyncData<T> and ExecuteAsync for automatic loading/error state management.
1
Define State
AuthState.cs
public record User(string Id, string Name, string Email);
public record AuthState(AsyncData<User> CurrentUser)
{
public static AuthState Initial => new(AsyncData<User>.NotAsked());
public bool IsAuthenticated => CurrentUser.HasData;
}
2
Create Login Component
Login.razor
@page "/login"
@inherits StoreComponent<AuthState>
@inject IAuthService AuthService
@if (State.CurrentUser.IsLoading)
{
<p>Logging in...</p>
}
else if (State.IsAuthenticated)
{
<p>Welcome, @State.CurrentUser.Data!.Name!</p>
<button @onclick="Logout">Logout</button>
}
else
{
<input @bind="email" placeholder="Email" />
<input @bind="password" type="password" />
<button @onclick="Login">Login</button>
@if (State.CurrentUser.HasError)
{
<p class="error">@State.CurrentUser.Error</p>
}
}
@code {
string email = "", password = "";
async Task Login() =>
await ExecuteAsync(
() => AuthService.LoginAsync(email, password),
loading: s => s with { CurrentUser = s.CurrentUser.ToLoading() },
success: (s, user) => s with { CurrentUser = AsyncData<User>.Success(user) },
error: (s, ex) => s with { CurrentUser = AsyncData<User>.Failure(ex.Message) }
);
async Task Logout() => await Update(s => AuthState.Initial);
}
Async Helpers in Action
- AsyncData<T>: Eliminates 95% of loading/error boilerplate
- ExecuteAsync: Automatic try-catch with state transitions
- Type-Safe: Compile-time checks for all states (NotAsked, Loading, Success, Failure)
Shopping Cart
A shopping cart implementation demonstrating computed properties, immutable updates, and complex state transformations.
CartState.cs
public record Product(string Id, string Name, decimal Price);
public record CartItem(Product Product, int Quantity);
public record CartState(ImmutableList<CartItem> Items)
{
public decimal Total => Items.Sum(i => i.Product.Price * i.Quantity);
public int ItemCount => Items.Sum(i => i.Quantity);
public CartState AddItem(Product product)
{
var existing = Items.FirstOrDefault(i => i.Product.Id == product.Id);
return existing != null
? this with { Items = Items.Replace(existing, existing with { Quantity = existing.Quantity + 1 }) }
: this with { Items = Items.Add(new CartItem(product, 1)) };
}
public CartState UpdateQuantity(string productId, int quantity) =>
quantity <= 0 ? RemoveItem(productId)
: this with {
Items = Items.Select(i =>
i.Product.Id == productId ? i with { Quantity = quantity } : i
).ToImmutableList()
};
public CartState RemoveItem(string productId) =>
this with { Items = Items.RemoveAll(i => i.Product.Id == productId) };
}
Pattern Highlights
- Computed Properties:
TotalandItemCountautomatically recalculate - Conditional Logic:
AddItem()handles both new items and quantity updates - Collection Operations: Safe immutable operations with
Replace(),RemoveAll()
Complete Example: Product Search
Combining multiple async helpers: UpdateDebounced, ExecuteAsync, AsyncData<T>, and LazyLoad.
ProductSearch.razor
@page "/products"
@inherits StoreComponent<ProductState>
<!-- 1. Debounced search -->
<input @oninput="@(e => UpdateDebounced(s => s.SetQuery(e.Value?.ToString() ?? ""), 300))"
placeholder="Search..." />
<!-- 2. Load with ExecuteAsync -->
<button @onclick="Search">Search</button>
<!-- 3. Display with AsyncData -->
@if (State.Products.IsLoading) { <p>Loading...</p> }
@if (State.Products.HasData)
{
@foreach (var product in State.Products.Data)
{
<div @onclick="@(() => LoadDetails(product.Id))">@product.Name</div>
}
}
@code {
async Task Search() =>
await ExecuteAsync(
() => ProductService.SearchAsync(State.Query),
loading: s => s with { Products = s.Products.ToLoading() },
success: (s, data) => s with { Products = AsyncData.Success(data) }
);
async Task LoadDetails(int id)
{
// 4. LazyLoad with caching
var details = await LazyLoad(
$"product-{id}",
() => ProductService.GetDetailsAsync(id),
TimeSpan.FromMinutes(5));
await Update(s => s.AddDetails(id, details));
}
}
74% Code Reduction
This example demonstrates how async helpers eliminate boilerplate:
- UpdateDebounced: No timer management needed (17 lines → 1 line)
- AsyncData<T>: No manual loading/error states (20+ lines → 1 property)
- ExecuteAsync: Automatic error handling (12 lines → 5 lines)
- LazyLoad: Built-in caching + deduplication (15+ lines → 2 lines)
Best Practices
✅ Do
// ✅ Use records for state
public record AppState(int Count, string Name);
// ✅ Use 'with' expressions
public AppState Increment() => this with { Count = Count + 1 };
// ✅ Use ImmutableList/ImmutableDictionary
public record State(ImmutableList<Item> Items);
// ✅ Keep state methods pure (no I/O, no logging)
public State AddItem(Item item) => this with { Items = Items.Add(item) };
// ✅ Batch updates when possible
await Update(s => s.SetLoading(true).ClearErrors().ResetForm());
❌ Don't
// ❌ Don't mutate state
public void Increment() { Count++; } // Wrong!
// ❌ Don't use mutable collections
public record State(List<Item> Items); // Wrong!
// ❌ Don't add side effects to state methods
public State AddItem(Item item)
{
_logger.Log("Adding"); // Wrong! Side effect!
return this with { Items = Items.Add(item) };
}
// ✅ Do side effects in components instead
@code {
async Task AddItem(Item item)
{
Logger.LogInformation("Adding item");
await Update(s => s.AddItem(item));
}
}