Middleware
Extend store behavior with custom middleware for cross-cutting concerns like logging, validation, and custom logic.
What is Middleware?
Middleware intercepts state updates before and after they happen, allowing you to add cross-cutting concerns without modifying your state logic. Think of it as a pipeline that wraps around every state update.
- Logging: Track state changes for debugging
- DevTools Integration: Time-travel debugging with Redux DevTools
- Persistence: Auto-save state to LocalStorage/SessionStorage
- Validation: Enforce business rules before updates
- Analytics: Track user actions and state changes
- Notifications: Trigger side effects on specific actions
IMiddleware Interface
All middleware implements the IMiddleware<TState> interface with two hooks:
public interface IMiddleware<TState> where TState : notnull
{
// Called BEFORE state is updated
Task OnBeforeUpdateAsync(TState currentState, string? action);
// Called AFTER state has been updated
Task OnAfterUpdateAsync(TState newState, string? action);
}
- Middleware executes in the order it was registered
- Both hooks are asynchronous to support I/O operations
- The
actionparameter is optional and can be null - Middleware should not modify state directly
Built-in Middleware
The library includes several production-ready middleware implementations:
Integrates with Redux DevTools browser extension for time-travel debugging, state inspection, and action replay.
Automatically saves state to LocalStorage or SessionStorage on updates and restores it on app load.
Logs state changes to the console or your logging provider for debugging and monitoring.
Use .WithDefaults(sp, "StoreName") to automatically add DevTools and Logging middleware:
builder.Services.AddStoreWithUtilities(
new AppState(),
(store, sp) => store.WithDefaults(sp, "MyApp")); // Includes DevTools + Logging
Creating Custom Middleware
Implement IMiddleware<TState> to create your own middleware:
Define the Middleware Class
public class LoggingMiddleware<TState> : IMiddleware<TState>
where TState : notnull
{
private readonly ILogger<LoggingMiddleware<TState>> _logger;
public LoggingMiddleware(ILogger<LoggingMiddleware<TState>> logger)
{
_logger = logger;
}
public Task OnBeforeUpdateAsync(TState currentState, string? action)
{
_logger.LogInformation(
"Before update - Action: {Action}, State: {@State}",
action ?? "Unknown",
currentState);
return Task.CompletedTask;
}
public Task OnAfterUpdateAsync(TState newState, string? action)
{
_logger.LogInformation(
"After update - Action: {Action}, New State: {@State}",
action ?? "Unknown",
newState);
return Task.CompletedTask;
}
}
Register the Middleware
// Option 1: Using WithMiddleware
builder.Services.AddStore(
new AppState(),
(store, sp) => store.WithMiddleware(
new LoggingMiddleware<AppState>(
sp.GetRequiredService<ILogger<LoggingMiddleware<AppState>>>())));
// Option 2: Manual builder setup
var logger = serviceProvider.GetRequiredService<ILogger<LoggingMiddleware<AppState>>>();
var middleware = new LoggingMiddleware<AppState>(logger);
builder.Services.AddStore(
new AppState(),
(store, sp) => store.WithMiddleware(middleware));
Advanced Examples
Validation Middleware
Enforce business rules before state updates:
public class ValidationMiddleware<TState> : IMiddleware<TState>
where TState : notnull
{
public Task OnBeforeUpdateAsync(TState currentState, string? action)
{
if (currentState is CartState cart && cart.Total > 10000)
{
throw new InvalidOperationException("Cart total exceeds limit");
}
return Task.CompletedTask;
}
public Task OnAfterUpdateAsync(TState newState, string? action)
=> Task.CompletedTask;
}
Analytics Middleware
Track user actions for analytics:
public class AnalyticsMiddleware<TState> : IMiddleware<TState>
where TState : notnull
{
private readonly IAnalyticsService _analytics;
public AnalyticsMiddleware(IAnalyticsService analytics)
{
_analytics = analytics;
}
public Task OnBeforeUpdateAsync(TState currentState, string? action)
=> Task.CompletedTask;
public async Task OnAfterUpdateAsync(TState newState, string? action)
{
if (!string.IsNullOrEmpty(action))
{
await _analytics.TrackEventAsync(new AnalyticsEvent
{
Name = action,
Properties = new { StateType = typeof(TState).Name }
});
}
}
}
Notification Middleware
Show toast notifications on specific actions:
public class NotificationMiddleware<TState> : IMiddleware<TState>
where TState : notnull
{
private readonly IToastService _toast;
public NotificationMiddleware(IToastService toast)
{
_toast = toast;
}
public Task OnBeforeUpdateAsync(TState currentState, string? action)
=> Task.CompletedTask;
public async Task OnAfterUpdateAsync(TState newState, string? action)
{
if (action == "ORDER_COMPLETED")
{
await _toast.ShowSuccessAsync("Order placed successfully!");
}
else if (action?.StartsWith("ERROR_") == true)
{
await _toast.ShowErrorAsync("An error occurred");
}
}
}
Middleware Pipeline
Multiple middleware instances execute in registration order:
builder.Services.AddStore(
new AppState(),
(store, sp) => store
.WithMiddleware(new ValidationMiddleware<AppState>())
.WithMiddleware(new LoggingMiddleware<AppState>(logger))
.WithMiddleware(new AnalyticsMiddleware<AppState>(analytics))
.WithPersistence(sp, "app-state")
.WithDevTools(sp, "MyApp"));
OnBeforeUpdateAsync: Validation → Logging → Analytics → Persistence → DevTools
OnAfterUpdateAsync: DevTools → Persistence → Analytics → Logging → Validation
Best Practices
Each middleware should have a single responsibility (logging, validation, etc.)
Middleware should observe state changes, not modify them. Use Update() in components instead.
Wrap async operations in try-catch to prevent middleware failures from breaking updates.
Always provide action names when updating: Update(s => s.Method(), "ACTION_NAME")
Keep middleware lightweight. Heavy processing should be done asynchronously or outside the update flow.
Validation should come first, persistence/DevTools last to ensure valid state is saved.