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: Total and ItemCount automatically 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));
    }
}

Next Steps

Async Helpers

Explore all 5 async helpers for maximum productivity.

Learn More →
📈

Performance

Optimize with selectors for 25x fewer re-renders.

View Guide →
🛠️

DevTools

Debug with Redux DevTools time-travel debugging.

Setup DevTools →