Skip to content

Commit 59f048c

Browse files
committed
Add diagnostics page and enhance rendering optimizations
- Introduced a new diagnostics page to monitor state changes and component renders. - Updated NavMenu to include a link to the diagnostics page. - Enhanced SelectorStoreComponent to optimize rendering by preventing unnecessary re-renders. - Improved diagnostics middleware integration for better performance tracking.
1 parent d02f8be commit 59f048c

5 files changed

Lines changed: 100 additions & 7 deletions

File tree

samples/EasyAppDev.Blazor.Store.Sample/Layout/NavMenu.razor

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,11 @@
2424
<span class="bi bi-list-nested-nav-menu" aria-hidden="true"></span> Weather
2525
</NavLink>
2626
</div>
27+
<div class="nav-item px-3">
28+
<NavLink class="nav-link" href="diagnostics">
29+
<span class="bi bi-speedometer2" aria-hidden="true"></span> Diagnostics
30+
</NavLink>
31+
</div>
2732
</nav>
2833
</div>
2934

samples/EasyAppDev.Blazor.Store.Sample/Pages/Counter.razor

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@
1212
<p><small>Last action: <strong>@State.LastAction</strong></small></p>
1313
}
1414

15+
<div class="alert alert-secondary mt-2">
16+
<strong>Render Count:</strong> @_renderCount
17+
<small class="text-muted"> (Expected: 1 per increment/decrement + 1 initial = @(State.Count + 1))</small>
18+
</div>
19+
1520
@* Note: This component uses SelectorStoreComponent and only re-renders when Count changes.
1621
Changes to LastAction alone will NOT trigger a re-render, improving performance. *@
1722

@@ -23,11 +28,18 @@
2328
</div>
2429

2530
@code {
26-
// Only re-render when Count changes (not when LastAction changes)
31+
private int _renderCount = 0;
32+
33+
// Granular selector: only re-render when Count changes
34+
// The base SelectorStoreComponent automatically prevents duplicate renders!
2735
protected override object SelectState(CounterState state)
2836
=> state.Count;
2937

30-
38+
protected override void OnAfterRender(bool firstRender)
39+
{
40+
base.OnAfterRender(firstRender);
41+
_renderCount++;
42+
}
3143
}
3244

3345

@@ -51,6 +63,8 @@
5163
</p>
5264
</div>
5365

54-
@* Diagnostic Panel - Only available in DEBUG builds *@
55-
<DiagnosticPanel DisplayMode="DiagnosticDisplayMode.Floating" />
66+
@* DiagnosticPanel kept on separate /diagnostics page.
67+
The Counter component only re-renders when Count changes (selector optimization).
68+
This means DiagnosticPanel updates for other events (subscriptions, non-Count state changes)
69+
would be blocked by ShouldRender(). For full diagnostic visibility, visit /diagnostics. *@
5670

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
@page "/diagnostics"
2+
3+
<PageTitle>Store Diagnostics</PageTitle>
4+
5+
<h1>Blazor Store Diagnostics</h1>
6+
7+
<p class="lead">
8+
Monitor state changes, component renders, performance metrics, and subscriptions in real-time.
9+
</p>
10+
11+
<div class="alert alert-info">
12+
<h4>About This Page</h4>
13+
<p>
14+
This diagnostics panel is on a separate page to prevent performance impacts on other components.
15+
The panel subscribes to diagnostic events and re-renders when data changes, which could cause
16+
unnecessary renders if embedded directly in production pages.
17+
</p>
18+
</div>
19+
20+
@* Diagnostic Panel - Only available in DEBUG builds *@
21+
#if DEBUG
22+
<DiagnosticPanel DisplayMode="DiagnosticDisplayMode.Inline" InitiallyCollapsed="false" />
23+
#else
24+
<div class="alert alert-warning">
25+
<h4>Diagnostics Not Available</h4>
26+
<p>Diagnostics are only available in DEBUG builds. Run the application with <code>dotnet run</code> (without --configuration Release) to enable diagnostics.</p>
27+
</div>
28+
#endif
29+
30+
<style>
31+
/* Make the inline diagnostics panel take full width */
32+
.blazor-store-diagnostics.inline {
33+
position: static;
34+
width: 100%;
35+
max-width: none;
36+
margin-top: 20px;
37+
}
38+
</style>

samples/EasyAppDev.Blazor.Store.Sample/Program.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@
3030
.WithLogging();
3131

3232
#if DEBUG
33-
// Add diagnostics middleware in DEBUG builds
33+
// Enable diagnostics middleware for tracking state changes and component renders
34+
// This is now safe with ShouldRender() optimization preventing cascade renders
3435
var diagnosticsService = sp.GetRequiredService<IDiagnosticsService>();
3536
storeBuilder = storeBuilder.WithDiagnostics(diagnosticsService);
3637
#endif

src/EasyAppDev.Blazor.Store/Blazor/SelectorStoreComponent.cs

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ public abstract class SelectorStoreComponent<TState> : ComponentBase, IDisposabl
3737
private IDisposable? _subscription;
3838
private bool _disposed;
3939
private object? _selectedValue;
40+
private object? _lastRenderedValue;
41+
private bool _isFirstRender = true;
4042
#if DEBUG
4143
private Guid _subscriptionId;
4244
#endif
@@ -177,12 +179,43 @@ protected override void OnInitialized()
177179
SubscribeToStore();
178180
}
179181

180-
#if DEBUG
182+
/// <inheritdoc />
183+
protected override bool ShouldRender()
184+
{
185+
// Always render on first render
186+
if (_isFirstRender)
187+
{
188+
return true;
189+
}
190+
191+
// Only render if the selected value has actually changed since last render
192+
// This prevents duplicate renders from Blazor's internal rendering mechanism
193+
var currentSelected = _selectedValue;
194+
195+
if (currentSelected == null && _lastRenderedValue == null)
196+
{
197+
return false;
198+
}
199+
200+
if (currentSelected == null || _lastRenderedValue == null)
201+
{
202+
return true;
203+
}
204+
205+
return !currentSelected.Equals(_lastRenderedValue);
206+
}
207+
181208
/// <inheritdoc />
182209
protected override void OnAfterRender(bool firstRender)
183210
{
184211
base.OnAfterRender(firstRender);
185212

213+
_isFirstRender = false;
214+
_lastRenderedValue = _selectedValue;
215+
216+
#if DEBUG
217+
// Record render event for diagnostics
218+
// This is now safe with ShouldRender() optimization preventing cascade renders
186219
if (DiagnosticsService is not null)
187220
{
188221
DiagnosticsService.RecordRender(new RenderEvent
@@ -194,8 +227,8 @@ protected override void OnAfterRender(bool firstRender)
194227
Reason = firstRender ? "Initial Render" : "Selector Value Changed"
195228
});
196229
}
197-
}
198230
#endif
231+
}
199232

200233
/// <summary>
201234
/// Subscribes to store changes using the selector pattern.
@@ -229,6 +262,8 @@ protected virtual void SubscribeToStore()
229262
callback: value =>
230263
{
231264
#if DEBUG
265+
// Record subscription notification for diagnostics
266+
// This is now safe with ShouldRender() optimization preventing cascade renders
232267
DiagnosticsService?.RecordSubscriptionNotification(_subscriptionId);
233268
#endif
234269
_selectedValue = value;

0 commit comments

Comments
 (0)