I spent 3 weekends building a full admin dashboard template with Blazor Web App (.NET 10). No component library, pure C# and CSS. Here are the 5 things that tripped me up — and that I couldn't find clearly documented anywhere.
1. @typeparam T conflicts with the Razor source generator
If you create a generic component with a single-letter type parameter like T, you'll get a cryptic CS0305 error pointing at a generated .g.cs file:
Error CS0305: Using the generic type 'DataGrid<T>' requires 1 type arguments
The fix is simple — rename your type parameter to something longer:
@* ❌ Breaks *@
@typeparam T
@* ✅ Works *@
@typeparam TItem
And update all references in the component accordingly. Not obvious from the error message at all.
2. You cannot pass RenderFragment to an InteractiveServer component
This one cost me a few hours. If you try to pass @Body or any RenderFragment parameter to a component with @rendermode InteractiveServer, you get this at runtime:
InvalidOperationException: Cannot pass the parameter 'Body' to component
'MyLayout' with rendermode 'InteractiveServerRenderMode'. This is because
the parameter is of the delegate type 'RenderFragment', which is arbitrary
code and cannot be serialized.
The rule: layouts must stay static. Only leaf components (components with no children passed as parameters) can be interactive.
This means you can't put @rendermode InteractiveServer on your MainLayout. Instead, put it on individual page components and child components.
3. event Action? on a singleton service is always null
A common pattern for notifying Blazor components from a singleton service is:
public class ThemeService
{
public event Action? OnChange;
public void Toggle()
{
IsDark = !IsDark;
OnChange?.Invoke(); // ← this is null more often than you think
}
}
The problem: if the component subscribes with OnChange += StateHasChanged in OnInitialized, but the layout is static (see point 2), OnInitialized never fires on the client. The event stays null.
The fix — use a List<Action> with explicit Subscribe/Unsubscribe:
public class ThemeService
{
private readonly List<Action> _listeners = new();
public void Subscribe(Action callback) => _listeners.Add(callback);
public void Unsubscribe(Action callback) => _listeners.Remove(callback);
public void Toggle()
{
IsDark = !IsDark;
foreach (var l in _listeners) l();
}
}
This gives you full control and avoids the null event trap.
4. StateHasChanged from a background thread needs InvokeAsync
When a singleton service notifies a component from a Task.Delay or a timer callback, you're on a background thread. Calling StateHasChanged directly will throw or silently do nothing.
// ❌ May fail silently
private void OnServiceChanged() => StateHasChanged();
// ✅ Correct
private void OnServiceChanged() => InvokeAsync(StateHasChanged);
// ✅ Also fine for fire-and-forget callbacks
private async void OnServiceChanged() => await InvokeAsync(StateHasChanged);
async void is generally discouraged in C#, but for Blazor component callbacks it's acceptable — the component lifecycle handles the exceptions.
5. Bootstrap class names conflict with your custom components
Blazor project templates ship with Bootstrap by default. Bootstrap defines .toast, .modal, .badge and others — with display: none or specific styling that will override or hide your custom components if you use the same class names.
/* Bootstrap does this internally */
.toast {
display: none; /* your toast will never show */
}
The fix — prefix all your custom component classes:
@* ❌ Conflicts with Bootstrap *@
<div class="toast">...</div>
@* ✅ Safe *@
<div class="ak-toast">...</div>
Simple but easy to miss when you're focused on the component logic.
What I built
These lessons came out of building AdminKit — a Blazor Web App admin dashboard template with:
- Generic
DataGrid<TItem>with sorting, search, pagination and row selection - Line, Bar and Donut charts via Chart.js
- Toast notification system (4 levels, auto-dismiss)
- Generic Modal component
- Light/dark theme with pure CSS variables
- Login, Profile, Forms and 404 pages
- Mobile responsive with hamburger menu
Live demo: https://adminkit-demonstration-exg2ezcma0gfd0dq.francecentral-01.azurewebsites.net
(Login: admin@demo.com / admin)
If you're building something with Blazor and want a head start, I released it as a paid template: https://dub.sh/adminkit.gr
Happy to answer questions about any of these gotchas in the comments.
Top comments (0)