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
  • IJSRuntime is 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
  • IJSRuntime always 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:

Program.cs - Same code works in Server, WASM, and Auto!
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)

Phase 1: Server Rendering (0-2 seconds)
  • 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)
↓
Phase 2: WASM Loading (background, 2-5 seconds)
  • .NET WebAssembly runtime downloads
  • User continues interacting with app
  • Store updates work normally
↓
Phase 3: WASM Active (seamless transition)
  • 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:

1

Blazor Server (Optimized)

Skip DevTools entirely to avoid initialization attempts:

builder.Services.AddStore(
    new CounterState(0),
    (store, sp) => store.WithLogging());  // Just logging, no DevTools
2

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
3

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:

1

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;
    }
}
2

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! πŸŽ‰)

Major Breakthrough!

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

Program.cs - Blazor Server with per-user isolation AND Redux DevTools!
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));
Key Breakthrough

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!
Complete Example with All Features
// βœ… 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));
    });
Limitations
  • ⚠️ LocalStorage persistence has limitations (sessions don't persist across reconnects)
  • ⚠️ Singleton stores still can't use DevTools (created before IJSRuntime exists)
Working Demo

See the Blazor Server Sample for a working demonstration of scoped stores with Redux DevTools!

Troubleshooting by Render Mode

Server Mode Issues
  • βœ… Store updates work? β†’ Core functionality is fine
  • ⚠️ DevTools not appearing? β†’ Expected behavior, use logging instead
  • ❌ Getting IJSRuntime errors? β†’ Remove .WithDefaults(), use .WithLogging()
WebAssembly Mode Issues
  • βœ… Everything works? β†’ You're all set!
  • ⚠️ DevTools not appearing? β†’ Check browser console, install Redux DevTools extension
Auto Mode Issues
  • ⚠️ 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

From Server to Auto:
  1. Add WebAssembly components: .AddInteractiveWebAssemblyComponents()
  2. Change render mode: @rendermode InteractiveAuto
  3. No code changes needed in state management!
From WASM to Auto:
  1. Add Server components: .AddInteractiveServerComponents()
  2. Change render mode: @rendermode InteractiveAuto
  3. No code changes needed in state management!
Key Takeaway

Write your state management code once with WithDefaults(), and it works perfectly across all render modes with automatic adaptation!

Next Steps