Skip to content

Commit d84c2ca

Browse files
committed
fixed blazor server persistence issue
1 parent 2cadac8 commit d84c2ca

5 files changed

Lines changed: 371 additions & 28 deletions

File tree

README.md

Lines changed: 267 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,9 @@ If you've used Zustand in React, you'll feel right at home.
4747
dotnet add package EasyAppDev.Blazor.Store
4848
```
4949

50-
**Requirements:** .NET 8.0+ • Blazor Server or WebAssembly • 38 KB gzipped
50+
**Requirements:** .NET 8.0+ • Blazor Server, WebAssembly, or Blazor Web App • 38 KB gzipped
51+
52+
> **Note for Blazor Server/Web App users**: See [compatibility notes](#-blazor-server--blazor-web-app-compatibility) below
5153
5254
### 1. Define Your State
5355

@@ -91,6 +93,251 @@ Inherit from `StoreComponent<T>` and call `Update()`. No actions, no reducers, n
9193

9294
---
9395

96+
## 🎯 Blazor Render Modes: Server, WebAssembly & Auto
97+
98+
**One library, three render modes, zero configuration changes!**
99+
100+
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.
101+
102+
### Quick Comparison
103+
104+
| Feature | Server | WebAssembly | Auto (Server→WASM) |
105+
|---------|--------|-------------|-------------------|
106+
| **Core State Management** | ✅ Full | ✅ Full | ✅ Full |
107+
| **Async Helpers** | ✅ All work | ✅ All work | ✅ All work |
108+
| **Components & Updates** | ✅ Perfect | ✅ Perfect | ✅ Perfect |
109+
| **Logging Middleware** | ✅ Works | ✅ Works | ✅ Works |
110+
| **Redux DevTools** | ⚠️ Gracefully skips | ✅ Works | ✅ Activates after transition |
111+
| **LocalStorage Persistence** | ❌ Not available | ✅ Works | ✅ Works after transition |
112+
| **Code Changes Needed** | ✅ None | ✅ None | ✅ None |
113+
114+
### Understanding Render Modes
115+
116+
#### 🟦 **Blazor Server**
117+
- Runs on the server via SignalR
118+
- UI updates sent over WebSocket
119+
- `IJSRuntime` is scoped (not available at startup)
120+
- **DevTools**: Gracefully skips (no JavaScript at startup)
121+
- **Persistence**: Not available (use server-side storage instead)
122+
123+
#### 🟩 **Blazor WebAssembly**
124+
- Runs entirely in browser
125+
- Downloads .NET runtime to client
126+
- `IJSRuntime` always available
127+
- **DevTools**: ✅ Full support
128+
- **Persistence**: ✅ Full support
129+
130+
#### 🟨 **Blazor Auto** (Server → WebAssembly)
131+
- **Phase 1**: Starts on server (fast initial load)
132+
- **Phase 2**: Downloads WASM in background
133+
- **Phase 3**: Seamlessly transitions to client-side
134+
- **DevTools**: Automatically activates after transition!
135+
- **Persistence**: Works after transition
136+
137+
### Universal Configuration (Works Everywhere!)
138+
139+
**Recommended setup for all modes:**
140+
```csharp
141+
// Program.cs - Same code works in Server, WASM, and Auto!
142+
builder.Services.AddStoreUtilities();
143+
144+
builder.Services.AddStore(
145+
new CounterState(0),
146+
(store, sp) => store.WithDefaults(sp, "Counter"));
147+
```
148+
149+
**What happens in each mode:**
150+
151+
| Render Mode | Behavior |
152+
|-------------|----------|
153+
| **Server** | DevTools silently skips, logging works, app runs perfectly |
154+
| **WebAssembly** | DevTools active immediately, all features work |
155+
| **Auto** | DevTools inactive initially, activates automatically after WASM loads |
156+
157+
### How Auto Mode Works (Behind the Scenes)
158+
159+
```
160+
┌─────────────────────────────────────────────────────────────┐
161+
│ Phase 1: Server Rendering (0-2 seconds) │
162+
├─────────────────────────────────────────────────────────────┤
163+
│ • User loads page │
164+
│ • Server renders HTML │
165+
│ • Store initializes with WithDefaults() │
166+
│ • DevTools tries to resolve IJSRuntime → Not available │
167+
│ • DevTools marks initialization as failed → Silent skip │
168+
│ • App works perfectly (core features unaffected) │
169+
└─────────────────────────────────────────────────────────────┘
170+
171+
┌─────────────────────────────────────────────────────────────┐
172+
│ Phase 2: WASM Loading (background, 2-5 seconds) │
173+
├─────────────────────────────────────────────────────────────┤
174+
│ • .NET WebAssembly runtime downloads │
175+
│ • User continues interacting with app │
176+
│ • Store updates work normally │
177+
└─────────────────────────────────────────────────────────────┘
178+
179+
┌─────────────────────────────────────────────────────────────┐
180+
│ Phase 3: WASM Active (seamless transition) │
181+
├─────────────────────────────────────────────────────────────┤
182+
│ • Next state update occurs │
183+
│ • DevTools tries to resolve IJSRuntime → Now available! │
184+
│ • DevTools initializes successfully │
185+
│ • Redux DevTools becomes active │
186+
│ • Persistence becomes available │
187+
│ • No user intervention needed! │
188+
└─────────────────────────────────────────────────────────────┘
189+
```
190+
191+
### Mode-Specific Configurations
192+
193+
While the universal configuration works everywhere, you can optimize for specific modes:
194+
195+
**Blazor Server (Optimized)**
196+
```csharp
197+
// Skip DevTools entirely to avoid initialization attempts
198+
builder.Services.AddStore(
199+
new CounterState(0),
200+
(store, sp) => store.WithLogging()); // Just logging, no DevTools
201+
```
202+
203+
**Blazor WebAssembly (Full Features)**
204+
```csharp
205+
// Enable all features including persistence
206+
builder.Services.AddStoreWithUtilities(
207+
new CounterState(0),
208+
(store, sp) => store
209+
.WithDefaults(sp, "Counter")
210+
.WithPersistence(sp, "counter-state")); // Auto-save to LocalStorage
211+
```
212+
213+
**Blazor Auto (Recommended Default)**
214+
```csharp
215+
// Use WithDefaults - DevTools activates automatically!
216+
builder.Services.AddStoreWithUtilities(
217+
new CounterState(0),
218+
(store, sp) => store.WithDefaults(sp, "Counter"));
219+
```
220+
221+
### Common Scenarios
222+
223+
#### Scenario 1: Pure Server App (No WASM)
224+
**Best approach**: Skip DevTools, use logging
225+
```csharp
226+
builder.Services.AddStore(
227+
new CounterState(0),
228+
(store, sp) => store.WithLogging());
229+
```
230+
231+
#### Scenario 2: Progressive Web App (Auto Mode)
232+
**Best approach**: Use WithDefaults, let it adapt
233+
```csharp
234+
builder.Services.AddStore(
235+
new CounterState(0),
236+
(store, sp) => store.WithDefaults(sp, "Counter"));
237+
```
238+
239+
#### Scenario 3: SPA with Full Client Features
240+
**Best approach**: Enable all features
241+
```csharp
242+
builder.Services.AddStoreWithUtilities(
243+
new CounterState(0),
244+
(store, sp) => store
245+
.WithDefaults(sp, "Counter")
246+
.WithPersistence(sp, "app-state"));
247+
```
248+
249+
### Persistence in Server Mode
250+
251+
Since LocalStorage isn't available in pure Server mode, here are alternatives:
252+
253+
**Option 1: Server-side storage**
254+
```csharp
255+
// Use database, session state, or distributed cache
256+
public record UserPreferences(string Theme, string Language)
257+
{
258+
public async Task<UserPreferences> SaveToDatabase(IDbContext db)
259+
{
260+
await db.SaveAsync(this);
261+
return this;
262+
}
263+
}
264+
```
265+
266+
**Option 2: Switch to Auto mode**
267+
```csharp
268+
// In Program.cs, add WASM support
269+
builder.Services.AddRazorComponents()
270+
.AddInteractiveServerComponents()
271+
.AddInteractiveWebAssemblyComponents(); // Enable Auto mode
272+
273+
// Then use @rendermode InteractiveAuto in components
274+
```
275+
276+
### Troubleshooting by Render Mode
277+
278+
**Server Mode Issues:**
279+
- ✅ Store updates work? → Core functionality is fine
280+
- ⚠️ DevTools not appearing? → Expected behavior, use logging instead
281+
- ❌ Getting IJSRuntime errors? → Remove `.WithDefaults()`, use `.WithLogging()`
282+
283+
**WebAssembly Mode Issues:**
284+
- ✅ Everything works? → You're all set!
285+
- ⚠️ DevTools not appearing? → Check browser console, install Redux DevTools extension
286+
287+
**Auto Mode Issues:**
288+
- ⚠️ DevTools delayed? → Normal, waits for WASM transition
289+
- ✅ Store works immediately? → Core features work from initial server render
290+
- ❌ Getting errors on startup? → Check that WASM components are registered
291+
292+
### Feature Detection
293+
294+
The library automatically detects and adapts:
295+
296+
```csharp
297+
// DevToolsMiddleware internal logic (simplified)
298+
private async Task EnsureInitializedAsync()
299+
{
300+
if (_initialized || _initializationFailed)
301+
return;
302+
303+
try
304+
{
305+
// Try to resolve IJSRuntime
306+
_jsRuntime = _serviceProvider.GetService<IJSRuntime>();
307+
308+
if (_jsRuntime == null)
309+
{
310+
_initializationFailed = true; // Server mode
311+
return;
312+
}
313+
314+
// Initialize DevTools
315+
await _jsRuntime.InvokeAsync(...);
316+
_initialized = true; // Success!
317+
}
318+
catch
319+
{
320+
_initializationFailed = true; // Graceful failure
321+
}
322+
}
323+
```
324+
325+
### Migration Paths
326+
327+
**From Server to Auto:**
328+
1. Add WebAssembly components: `.AddInteractiveWebAssemblyComponents()`
329+
2. Change render mode: `@rendermode InteractiveAuto`
330+
3. No code changes needed in state management!
331+
332+
**From WASM to Auto:**
333+
1. Add Server components: `.AddInteractiveServerComponents()`
334+
2. Change render mode: `@rendermode InteractiveAuto`
335+
3. No code changes needed in state management!
336+
337+
> **Key Takeaway**: Write your state management code once with `WithDefaults()`, and it works perfectly across all render modes with automatic adaptation!
338+
339+
---
340+
94341
## Table of Contents
95342

96343
- [Quick Start](#quick-start)
@@ -703,6 +950,25 @@ public State AddItem(Item item)
703950

704951
## Troubleshooting
705952

953+
### "Cannot resolve scoped service 'IJSRuntime' from root provider"?
954+
**UPDATED**: This error no longer occurs! The library now uses lazy IJSRuntime resolution.
955+
956+
**If you're seeing this error**, you're using an old configuration pattern:
957+
```csharp
958+
// ❌ Old pattern (caused errors)
959+
var jsRuntime = serviceProvider.GetRequiredService<IJSRuntime>();
960+
builder.Services.AddStore(new State(), store => store.WithDevTools(jsRuntime, "Store"));
961+
962+
// ✅ New pattern (works everywhere!)
963+
builder.Services.AddStore(
964+
new State(),
965+
(store, sp) => store.WithDefaults(sp, "Store")); // Lazy resolution!
966+
```
967+
968+
The new `WithDefaults(sp, ...)` method resolves IJSRuntime lazily, so it works in **all render modes** (Server, WASM, Auto).
969+
970+
See the [Blazor Render Modes](#-blazor-render-modes-server-webassembly--auto) section for details.
971+
706972
### Component not updating?
707973
✅ Inherit from `StoreComponent<TState>`
708974

src/EasyAppDev.Blazor.Store/Blazor/StoreBuilderExtensions.cs

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ namespace EasyAppDev.Blazor.Store.Blazor;
1414
public static class StoreBuilderExtensions
1515
{
1616
/// <summary>
17-
/// Applies default middleware configuration: JSRuntime, DevTools, and Logging.
17+
/// Applies default middleware configuration: DevTools and Logging.
18+
/// Works in all render modes (Server, WebAssembly, Auto) with lazy IJSRuntime resolution.
1819
/// </summary>
1920
/// <typeparam name="TState">The type of state.</typeparam>
2021
/// <param name="builder">The store builder.</param>
@@ -27,30 +28,33 @@ public static StoreBuilder<TState> WithDefaults<TState>(
2728
string? storeName = null)
2829
where TState : notnull
2930
{
30-
var jsRuntime = serviceProvider.GetRequiredService<IJSRuntime>();
31-
32-
return builder.WithJSRuntime(jsRuntime)
33-
.WithDevTools(storeName ?? typeof(TState).Name)
31+
// Use service provider-based DevTools for lazy IJSRuntime resolution
32+
// Works in Server (gracefully fails), WASM (succeeds), and Auto (Server→WASM transition)
33+
return builder.WithDevTools(serviceProvider, storeName ?? typeof(TState).Name)
3434
.WithLogging();
3535
}
3636

3737
/// <summary>
3838
/// Adds persistence middleware with automatic LocalStorageProvider creation.
39+
/// Note: In Blazor Server/United, this method is disabled due to IJSRuntime scoping issues.
40+
/// Use AddScopedStore with persistence instead.
3941
/// </summary>
4042
/// <typeparam name="TState">The type of state.</typeparam>
4143
/// <param name="builder">The store builder.</param>
4244
/// <param name="serviceProvider">The service provider for resolving IJSRuntime.</param>
4345
/// <param name="key">The storage key for persisting state.</param>
4446
/// <returns>The configured builder for chaining.</returns>
47+
[Obsolete("Persistence with singleton stores doesn't work in Blazor Server/United due to scoped IJSRuntime. Persistence is disabled.")]
4548
public static StoreBuilder<TState> WithPersistence<TState>(
4649
this StoreBuilder<TState> builder,
4750
IServiceProvider serviceProvider,
4851
string key)
4952
where TState : notnull
5053
{
51-
var jsRuntime = serviceProvider.GetRequiredService<IJSRuntime>();
52-
var localStorage = new LocalStorageProvider(jsRuntime);
53-
return builder.WithPersistence(localStorage, key);
54+
// Skip persistence in Blazor Server/United scenarios
55+
// IJSRuntime is scoped and cannot be resolved during singleton store creation
56+
Console.WriteLine($"Warning: Persistence skipped for {typeof(TState).Name}. Use AddScopedStore for persistence in Blazor Server/United.");
57+
return builder;
5458
}
5559

5660
/// <summary>

src/EasyAppDev.Blazor.Store/Core/StoreBuilder.cs

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -155,24 +155,46 @@ public StoreBuilder<TState> WithJSRuntime(IJSRuntime jsRuntime)
155155
}
156156

157157
/// <summary>
158-
/// Enables Redux DevTools integration.
158+
/// Enables Redux DevTools integration with lazy IJSRuntime resolution.
159+
/// Works in all render modes: Server, WebAssembly, and Auto (Server → WASM).
159160
/// </summary>
161+
/// <param name="serviceProvider">Service provider to resolve IJSRuntime on-demand.</param>
160162
/// <param name="storeName">The name to display in DevTools. Defaults to the state type name.</param>
161163
/// <returns>The builder instance for chaining.</returns>
162-
/// <exception cref="InvalidOperationException">Thrown when JSRuntime has not been set.</exception>
163-
public StoreBuilder<TState> WithDevTools(string? storeName = null)
164+
public StoreBuilder<TState> WithDevTools(IServiceProvider serviceProvider, string? storeName = null)
164165
{
165-
if (_jsRuntime == null)
166-
throw new InvalidOperationException(
167-
"JSRuntime must be provided. Use WithJSRuntime() first or pass IJSRuntime to WithDevTools().");
166+
ArgumentNullException.ThrowIfNull(serviceProvider);
168167

169168
var devToolsMiddleware = new DevToolsMiddleware<TState>(
170-
_jsRuntime,
169+
serviceProvider,
171170
storeName ?? typeof(TState).Name);
172171

173172
return WithMiddleware(devToolsMiddleware);
174173
}
175174

175+
/// <summary>
176+
/// Enables Redux DevTools integration.
177+
/// Note: This overload is deprecated. Use WithDevTools(IServiceProvider, string?) instead.
178+
/// </summary>
179+
/// <param name="storeName">The name to display in DevTools. Defaults to the state type name.</param>
180+
/// <returns>The builder instance for chaining.</returns>
181+
[Obsolete("Use WithDevTools(IServiceProvider serviceProvider, string? storeName) instead for better compatibility with all render modes.")]
182+
public StoreBuilder<TState> WithDevTools(string? storeName = null)
183+
{
184+
// Skip DevTools if JSRuntime is not available (Blazor Server scenario)
185+
// DevTools middleware will be added later if needed
186+
if (_jsRuntime != null)
187+
{
188+
var devToolsMiddleware = new DevToolsMiddleware<TState>(
189+
_jsRuntime,
190+
storeName ?? typeof(TState).Name);
191+
192+
return WithMiddleware(devToolsMiddleware);
193+
}
194+
195+
return this;
196+
}
197+
176198
/// <summary>
177199
/// Enables Redux DevTools integration with an explicit JSRuntime.
178200
/// </summary>

0 commit comments

Comments
 (0)