Best Practices

Follow these guidelines to write clean, maintainable state management code with EasyAppDev.Blazor.Store.

✅ Do's - Follow These Patterns

Use Records for State

Always define state as C# records to leverage immutability and structural equality.

✅ Correct
public record AppState(int Count, string Name);

Use 'with' Expressions

Always use with expressions to create modified copies of state.

✅ Correct
public AppState Increment() => this with { Count = Count + 1 };

Use Immutable Collections

Use ImmutableList and ImmutableDictionary for collections in your state.

✅ Correct
public record State(ImmutableList<Item> Items);

Keep State Methods Pure

State methods should be pure functions with no side effects, I/O, or logging.

✅ Correct
public State AddItem(Item item) => this with { Items = Items.Add(item) };

Batch Updates When Possible

Chain multiple state transformations into a single update to minimize re-renders.

✅ Correct
await UpdateAsync(s => s.SetLoading(true).ClearErrors().ResetForm());

❌ Don'ts - Avoid These Anti-Patterns

Don't Mutate State

Never mutate state properties directly. This breaks immutability and reactivity.

❌ Wrong
public void Increment() { Count++; }  // Wrong! Mutates state
✅ Correct
public AppState Increment() => this with { Count = Count + 1 };

Don't Use Mutable Collections

Avoid List<T>, Dictionary<K,V>, and other mutable collections.

❌ Wrong
public record State(List<Item> Items);  // Wrong! Mutable collection
✅ Correct
public record State(ImmutableList<Item> Items);

Don't Add Side Effects to State Methods

State methods must be pure functions. Put side effects (logging, API calls) in components.

❌ Wrong
public State AddItem(Item item)
{
    _logger.Log("Adding");  // Wrong! Side effect!
    return this with { Items = Items.Add(item) };
}
✅ Correct - Do Side Effects in Components
@code {
    async Task AddItem(Item item)
    {
        Logger.LogInformation("Adding item");
        await UpdateAsync(s => s.AddItem(item));
    }
}

🔧 Troubleshooting

Common issues and their solutions:

Component not updating?

Make sure your component inherits from StoreComponent<TState>.

@inherits StoreComponent<CounterState>
State not changing?

Always use with expressions to create new state instances.

// ✅ Correct
return state with { Count = 5 };

// ❌ Wrong
state.Count = 5;
return state;
Collections not updating?

Use ImmutableList and its transformation methods like Add(), Remove(), RemoveAll().

// ✅ Correct
return this with { Items = Items.Add(newItem) };

// ❌ Wrong
Items.Add(newItem);
return this;
Async operations failing?

Use UpdateAsync instead of Update for async state methods.

// ✅ Correct
await UpdateAsync(async s => await s.LoadDataAsync());

// ❌ Wrong
await UpdateAsync(async s => await s.LoadDataAsync());
ExecuteAsync not available?

Make sure you registered the store utilities in Program.cs.

// ✅ Use this registration method
builder.Services.AddStoreWithUtilities(...);
UpdateDebounced/Throttle/LazyLoad not available?

Register utility services before using async helpers.

// ✅ All-in-one registration
builder.Services.AddStoreWithUtilities(...);

// OR manually
builder.Services.AddStoreUtilities();

⚠️ Common Pitfalls

⚠️ Forgetting 'with' expressions

Always use this with { Property = value } instead of direct assignment.

⚠️ Using Update instead of UpdateAsync

For async state methods, use UpdateAsync(async s => await s.Method()).

⚠️ Mutating collections

Use ImmutableList.Add() not List.Add().

⚠️ Side effects in state methods

Keep state methods pure. No logging, no API calls, no database access.

⚠️ Forgetting optional action names

While optional, providing action names helps with debugging: Update(s => s.Method(), "ACTION_NAME").

⚠️ Not registering utilities

Always call AddStoreUtilities() or use AddStoreWithUtilities() before using StoreComponent<T>.

⚠️ Missing async executor registration

Each state type needs AddAsyncActionExecutor<TState>() to use ExecuteAsync. Or use AddStoreWithUtilities().

💡 Additional Tips

Performance Optimization

Use SelectorStoreComponent for granular subscriptions to prevent unnecessary re-renders.

@inherits SelectorStoreComponent<AppState>

@code {
    // Only re-render when Counter changes
    protected override object SelectState(AppState state) => state.Counter;
}
Derived State

Use computed properties in your state records for derived values.

public record TodoState(ImmutableList<Todo> Todos)
{
    public int CompletedCount => Todos.Count(t => t.Completed);
    public double CompletionRate => Todos.Count > 0
        ? (double)CompletedCount / Todos.Count * 100 : 0;
}
Debugging with DevTools

Enable Redux DevTools integration for time-travel debugging and state inspection.

builder.Services.AddStoreWithUtilities(
    new AppState(),
    (store, sp) => store.WithDefaults(sp, "MyApp"));  // DevTools included!

Next Steps