Blazor Render Modes: Server, WebAssembly & Auto
One library, three render modes, zero configuration changes!
The library automatically adapts to your Blazor render mode with intelligent lazy initialization. Use the same code everywhere and let the library handle the differences.
Quick Comparison
| Feature | Server (Singleton) | Server (Scoped) | WebAssembly | Auto (ServerβWASM) |
|---|---|---|---|---|
| Core State Management | β Full | β Full | β Full | β Full |
| Async Helpers | β All work | β All work | β All work | β All work |
| Components & Updates | β Perfect | β Perfect | β Perfect | β Perfect |
| Logging Middleware | β Works | β Works | β Works | β Works |
| Redux DevTools | β οΈ Gracefully skips | β Works! | β Works | β Activates after transition |
| LocalStorage Persistence | β Not available | β οΈ Limited | β Works | β Works after transition |
| Code Changes Needed | β None | β None | β None | β None |
Understanding Render Modes
Blazor Server
- Runs on the server via SignalR
- UI updates sent over WebSocket
IJSRuntimeis scoped (not available at startup)- DevTools: Gracefully skips (no JavaScript at startup)
- Persistence: Not available (use server-side storage instead)
Blazor WebAssembly
- Runs entirely in browser
- Downloads .NET runtime to client
IJSRuntimealways available- DevTools: β Full support
- Persistence: β Full support
Blazor Auto (Server β WebAssembly)
- Phase 1: Starts on server (fast initial load)
- Phase 2: Downloads WASM in background
- Phase 3: Seamlessly transitions to client-side
- DevTools: Automatically activates after transition!
- Persistence: Works after transition
Universal Configuration (Works Everywhere!)
Recommended setup for all modes:
builder.Services.AddStoreUtilities();
builder.Services.AddStore(
new CounterState(0),
(store, sp) => store.WithDefaults(sp, "Counter"));
What happens in each mode:
| Render Mode | Behavior |
|---|---|
| Server | DevTools silently skips, logging works, app runs perfectly |
| WebAssembly | DevTools active immediately, all features work |
| Auto | DevTools inactive initially, activates automatically after WASM loads |
How Auto Mode Works (Behind the Scenes)
- User loads page
- Server renders HTML
- Store initializes with WithDefaults()
- DevTools tries to resolve IJSRuntime β Not available
- DevTools marks initialization as failed β Silent skip
- App works perfectly (core features unaffected)
- .NET WebAssembly runtime downloads
- User continues interacting with app
- Store updates work normally
- Next state update occurs
- DevTools tries to resolve IJSRuntime β Now available!
- DevTools initializes successfully
- Redux DevTools becomes active
- Persistence becomes available
- No user intervention needed!
Mode-Specific Configurations
While the universal configuration works everywhere, you can optimize for specific modes:
Blazor Server (Optimized)
Skip DevTools entirely to avoid initialization attempts:
builder.Services.AddStore(
new CounterState(0),
(store, sp) => store.WithLogging()); // Just logging, no DevTools
Blazor WebAssembly (Full Features)
Enable all features including persistence:
builder.Services.AddStoreWithUtilities(
new CounterState(0),
(store, sp) => store
.WithDefaults(sp, "Counter")
.WithPersistence(sp, "counter-state")); // Auto-save to LocalStorage
Blazor Auto (Recommended Default)
Use WithDefaults - DevTools activates automatically:
builder.Services.AddStoreWithUtilities(
new CounterState(0),
(store, sp) => store.WithDefaults(sp, "Counter"));
Common Scenarios
Scenario 1: Pure Server App
Best approach: Skip DevTools, use logging
builder.Services.AddStore(
new CounterState(0),
(store, sp) => store.WithLogging());
Scenario 2: Progressive Web App
Best approach: Use WithDefaults, let it adapt
builder.Services.AddStore(
new CounterState(0),
(store, sp) => store.WithDefaults(sp, "Counter"));
Scenario 3: SPA with Full Features
Best approach: Enable all features
builder.Services.AddStoreWithUtilities(
new CounterState(0),
(store, sp) => store
.WithDefaults(sp, "Counter")
.WithPersistence(sp, "app-state"));
Persistence in Server Mode
Since LocalStorage isn't available in pure Server mode, here are alternatives:
Option 1: Server-side storage
// Use database, session state, or distributed cache
public record UserPreferences(string Theme, string Language)
{
public async Task<UserPreferences> SaveToDatabase(IDbContext db)
{
await db.SaveAsync(this);
return this;
}
}
Option 2: Switch to Auto mode
// In Program.cs, add WASM support
builder.Services.AddRazorComponents()
.AddInteractiveServerComponents()
.AddInteractiveWebAssemblyComponents(); // Enable Auto mode
// Then use @rendermode InteractiveAuto in components
Scoped Stores for Blazor Server (Now with Redux DevTools! π)
Scoped stores in Blazor Server now support Redux DevTools! This was previously thought impossible, but we've made it work.
The Problem
In Blazor Server, singleton stores are shared across all SignalR circuits (all connected users). This means every client sees the same state, which is not ideal for user-specific data.
The Solution
Use scoped stores for per-circuit (per-user) isolation. Each user gets their own instance of the store.
When to Use Scoped Stores
| Scenario | Store Type |
|---|---|
| User-specific data (cart, preferences, session) | β Scoped |
| Shared app data (product catalog, config) | β Singleton |
| WebAssembly apps | β Singleton (no sharing issue) |
Scoped Store Registration
builder.Services.AddStoreUtilities();
// Scoped store with DevTools - each user gets their own instance
builder.Services.AddScopedStore(
new UserSessionState(),
(store, sp) => store
.WithDevTools(sp, "User Session") // β
Redux DevTools work!
.WithLogging());
// Or with utilities (async helpers) + full features
builder.Services.AddScopedStoreWithUtilities(
new ShoppingCartState(),
(store, sp) => store
.WithDefaults(sp, "Shopping Cart") // DevTools + Logging
.WithMiddleware(customMiddleware));
Scoped stores now accept IServiceProvider in the configure callback, which enables:
- β Redux DevTools (IJSRuntime resolved per-circuit!)
- β Access to scoped services (middleware, validators)
- β Full feature parity with WebAssembly mode
Why DevTools Work with Scoped Stores
The key difference:
- Singleton stores: Created at app startup before any circuits exist β No IJSRuntime available
- Scoped stores: Created when SignalR circuits are established β IJSRuntime is available!
// β
DevTools WORK with scoped stores!
builder.Services.AddScopedStore(
new UserSessionState(),
(store, sp) => store
.WithDevTools(sp, "User Session") // β
Works perfectly!
.WithLogging());
// β
Complete example with all features
builder.Services.AddScopedStore(
new ShoppingCartState(),
(store, sp) => store
.WithDefaults(sp, "Shopping Cart") // Includes DevTools + Logging
.WithMiddleware(customMiddleware));
Accessing Scoped Services
The new signature enables dependency injection:
// Register scoped validator
builder.Services.AddScoped<IStateValidator<CartState>, CartValidator>();
// Access it in configure callback
builder.Services.AddScopedStore(
new CartState(),
(store, sp) =>
{
var validator = sp.GetRequiredService<IStateValidator<CartState>>();
return store.WithMiddleware(new ValidationMiddleware<CartState>(validator));
});
- β οΈ LocalStorage persistence has limitations (sessions don't persist across reconnects)
- β οΈ Singleton stores still can't use DevTools (created before IJSRuntime exists)
See the Blazor Server Sample for a working demonstration of scoped stores with Redux DevTools!
Troubleshooting by Render Mode
- β Store updates work? β Core functionality is fine
- β οΈ DevTools not appearing? β Expected behavior, use logging instead
- β Getting IJSRuntime errors? β Remove
.WithDefaults(), use.WithLogging()
- β Everything works? β You're all set!
- β οΈ DevTools not appearing? β Check browser console, install Redux DevTools extension
- β οΈ DevTools delayed? β Normal, waits for WASM transition
- β Store works immediately? β Core features work from initial server render
- β Getting errors on startup? β Check that WASM components are registered
Migration Paths
- Add WebAssembly components:
.AddInteractiveWebAssemblyComponents() - Change render mode:
@rendermode InteractiveAuto - No code changes needed in state management!
- Add Server components:
.AddInteractiveServerComponents() - Change render mode:
@rendermode InteractiveAuto - No code changes needed in state management!
Write your state management code once with WithDefaults(), and it works perfectly across all render modes with automatic adaptation!