dotnet-blazor-components
SKILL.md
dotnet-blazor-components
Blazor component architecture: lifecycle methods, state management (cascading values, DI, browser storage), JavaScript interop (AOT-safe), EditForm validation, and QuickGrid. Covers per-render-mode behavior differences where relevant.
Scope
- Component lifecycle methods (SetParametersAsync, OnInitialized, OnAfterRender)
- State management (cascading values, DI, browser storage)
- JavaScript interop (AOT-safe module imports)
- EditForm validation and input components
- QuickGrid data binding and virtualization
- Per-render-mode behavior differences (Static SSR, InteractiveServer, WASM)
Out of scope
- Hosting model selection and render modes -- see [skill:dotnet-blazor-patterns]
- Auth components (AuthorizeView, CascadingAuthenticationState) -- see [skill:dotnet-blazor-auth]
- bUnit testing -- see [skill:dotnet-blazor-testing]
- Standalone SignalR hub patterns -- see [skill:dotnet-realtime-communication]
- E2E testing -- see [skill:dotnet-playwright]
- UI framework selection -- see [skill:dotnet-ui-chooser]
- Accessibility patterns (ARIA, keyboard navigation) -- see [skill:dotnet-accessibility]
Cross-references: [skill:dotnet-blazor-patterns] for hosting models and render modes, [skill:dotnet-blazor-auth] for authentication, [skill:dotnet-blazor-testing] for bUnit testing, [skill:dotnet-realtime-communication] for standalone SignalR, [skill:dotnet-playwright] for E2E testing, [skill:dotnet-ui-chooser] for framework selection, [skill:dotnet-accessibility] for accessibility patterns (ARIA, keyboard nav, screen readers).
Component Lifecycle
Lifecycle Methods
@code {
// 1. Called when parameters are set/updated
public override async Task SetParametersAsync(ParameterView parameters)
{
// Access raw parameters before they are applied
await base.SetParametersAsync(parameters);
}
// 2. Called after parameters are assigned (sync)
protected override void OnInitialized()
{
// One-time initialization (runs once per component instance)
}
// 3. Called after parameters are assigned (async)
protected override async Task OnInitializedAsync()
{
// Async initialization (data fetching, service calls)
products = await ProductService.GetProductsAsync();
}
// 4. Called every time parameters change
protected override void OnParametersSet()
{
// React to parameter changes
}
// 5. Called after each render
protected override void OnAfterRender(bool firstRender)
{
if (firstRender)
{
// JS interop safe here -- DOM is available
}
}
// 6. Async version of OnAfterRender
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
await JSRuntime.InvokeVoidAsync("initializeChart", chartElement);
}
}
// 7. Cleanup
public void Dispose()
{
// Unsubscribe from events, dispose resources
}
// 8. Async cleanup
public async ValueTask DisposeAsync()
{
// Async cleanup (dispose JS object references)
if (module is not null)
{
await module.DisposeAsync();
}
}
}
```text
### Lifecycle Behavior per Render Mode
| Lifecycle Event | Static SSR | InteractiveServer | InteractiveWebAssembly | InteractiveAuto | Hybrid |
| ---------------------- | --------------------- | -------------------------------------------- | -------------------------------- | ------------------------------------------------------------------ | --------------------------- |
| `OnInitialized(Async)` | Runs on server | Runs on server | Runs in browser | Server on first load, browser after WASM cached | Runs in-process |
| `OnAfterRender(Async)` | Never called | Runs on server after SignalR confirms render | Runs in browser after DOM update | Server-side then browser-side (matches active runtime) | Runs after WebView render |
| `Dispose(Async)` | Called after response | Called when circuit ends | Called on component removal | Called when circuit ends (Server phase) or on removal (WASM phase) | Called on component removal |
**Gotcha:** In Static SSR, `OnAfterRender` never executes because there is no persistent connection. Do not place
critical logic in `OnAfterRender` for Static SSR pages.
---
## State Management
### Cascading Values
Cascading values flow data down the component tree without explicit parameter passing.
```razor
<!-- Parent: provide a cascading value -->
<CascadingValue Value="@theme" Name="AppTheme">
<Router AppAssembly="typeof(App).Assembly">
<!-- All descendants can receive AppTheme -->
</Router>
</CascadingValue>
@code {
private ThemeSettings theme = new() { IsDarkMode = false, AccentColor = "#0078d4" };
}
```text
```razor
<!-- Child: consume the cascading value -->
@code {
[CascadingParameter(Name = "AppTheme")]
public ThemeSettings? Theme { get; set; }
}
```text
**Fixed cascading values (.NET 8+):** For values that never change after initial render, use `IsFixed="true"` to avoid
re-render overhead:
```razor
<CascadingValue Value="@config" IsFixed="true">
<ChildComponent />
</CascadingValue>
```text
### Dependency Injection
```csharp
// Register services in Program.cs
builder.Services.AddScoped<IProductService, ProductService>();
builder.Services.AddSingleton<AppState>();
// Inject in components
@inject IProductService ProductService
@inject AppState State
```text
**DI lifetime behavior per render mode:**
| Lifetime | InteractiveServer | InteractiveWebAssembly | InteractiveAuto | Hybrid |
| --------- | ---------------------------------------- | --------------------------------------- | ------------------------------------------------------------------------------------------ | ---------------------------------------- |
| Singleton | Shared across all circuits on the server | One per browser tab | Server-shared during Server phase; per-tab after WASM switch | One per app instance |
| Scoped | One per circuit (acts like per-user) | One per browser tab (same as Singleton) | Per-circuit (Server phase), per-tab (WASM phase) -- state does not transfer between phases | One per app instance (same as Singleton) |
| Transient | New instance each injection | New instance each injection | New instance each injection | New instance each injection |
**Gotcha:** In Blazor Server, `Scoped` services live for the entire circuit duration (not per-request like in MVC). A
circuit persists until the user navigates away or the connection drops. Long-lived scoped services may accumulate state
-- use `OwningComponentBase<T>` for component-scoped DI.
### Browser Storage
```csharp
// ProtectedBrowserStorage -- encrypted, per-user storage
// Available in InteractiveServer only (not WASM -- server encrypts/decrypts)
@inject ProtectedSessionStorage SessionStorage
@inject ProtectedLocalStorage LocalStorage
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
// Session storage (cleared when tab closes)
await SessionStorage.SetAsync("cart", cartItems);
var result = await SessionStorage.GetAsync<List<CartItem>>("cart");
if (result.Success) { cartItems = result.Value!; }
// Local storage (persists across sessions)
await LocalStorage.SetAsync("preferences", userPrefs);
}
}
```text
For InteractiveWebAssembly, use JS interop to access browser storage directly:
```csharp
// WASM: Direct browser storage via JS interop
await JSRuntime.InvokeVoidAsync("localStorage.setItem", "key",
JsonSerializer.Serialize(value, AppJsonContext.Default.UserPrefs));
var json = await JSRuntime.InvokeAsync<string?>("localStorage.getItem", "key");
if (json is not null)
{
value = JsonSerializer.Deserialize(json, AppJsonContext.Default.UserPrefs);
}
```json
**Gotcha:** `ProtectedBrowserStorage` is not available during prerendering. Always access it in
`OnAfterRenderAsync(firstRender: true)`, never in `OnInitializedAsync`.
---
## JavaScript Interop
### Calling JavaScript from .NET
```csharp
@inject IJSRuntime JSRuntime
// Invoke a global JS function
await JSRuntime.InvokeVoidAsync("console.log", "Hello from Blazor");
// Invoke and get a return value
var width = await JSRuntime.InvokeAsync<int>("getWindowWidth");
// With timeout (important for Server to avoid hanging circuits)
var result = await JSRuntime.InvokeAsync<string>(
"expensiveOperation",
TimeSpan.FromSeconds(10),
inputData);
```text
### JavaScript Module Imports (AOT-Safe)
```csharp
// Import a JS module -- trim-safe, no reflection
private IJSObjectReference? module;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
module = await JSRuntime.InvokeAsync<IJSObjectReference>(
"import", "./js/interop.js");
await module.InvokeVoidAsync("initialize", elementRef);
}
}
// Always dispose module references
public async ValueTask DisposeAsync()
{
if (module is not null)
{
await module.DisposeAsync();
}
}
```text
```javascript
// wwwroot/js/interop.js
export function initialize(element) {
// Set up the element
}
export function getValue(element) {
return element.value;
}
```text
### Calling .NET from JavaScript
```csharp
// Instance method callback
private DotNetObjectReference<MyComponent>? dotNetRef;
protected override void OnInitialized()
{
dotNetRef = DotNetObjectReference.Create(this);
}
[JSInvokable]
public void OnJsEvent(string data)
{
message = data;
StateHasChanged();
}
public void Dispose()
{
dotNetRef?.Dispose();
}
```text
```javascript
// Call .NET from JS
export function registerCallback(dotNetRef) {
document.addEventListener('custom-event', e => {
dotNetRef.invokeMethodAsync('OnJsEvent', e.detail);
});
}
```text
### JS Interop per Render Mode
| Concern | InteractiveServer | InteractiveWebAssembly | InteractiveAuto | Hybrid |
| ------------------------- | ----------------------------- | ------------------------------- | ----------------------------------------------------------------------- | ------------------------------- |
| JS call timing | After SignalR confirms render | After WASM runtime loads | SignalR initially, then direct after WASM switch | After WebView loads |
| `OnAfterRender` available | Yes | Yes | Yes | Yes |
| IJSRuntime sync calls | Not supported (async only) | `IJSInProcessRuntime` available | Async-only during Server phase; `IJSInProcessRuntime` after WASM switch | `IJSInProcessRuntime` available |
| Module imports | Via SignalR (latency) | Direct (fast) | SignalR (Server phase), direct (WASM phase) | Direct (fast) |
**Gotcha:** In InteractiveServer, all JS interop calls travel over SignalR, adding network latency. Minimize round trips
by batching operations into a single JS function call.
---
## EditForm Validation
### Basic EditForm with Data Annotations
```razor
<EditForm Model="product" OnValidSubmit="HandleSubmit" FormName="product-form">
<DataAnnotationsValidator />
<ValidationSummary />
<div>
<label for="name">Name:</label>
<InputText id="name" @bind-Value="product.Name" />
<ValidationMessage For="() => product.Name" />
</div>
<div>
<label for="price">Price:</label>
<InputNumber id="price" @bind-Value="product.Price" />
<ValidationMessage For="() => product.Price" />
</div>
<div>
<label for="category">Category:</label>
<InputSelect id="category" @bind-Value="product.Category">
<option value="">Select...</option>
<option value="Electronics">Electronics</option>
<option value="Clothing">Clothing</option>
</InputSelect>
<ValidationMessage For="() => product.Category" />
</div>
<button type="submit">Save</button>
</EditForm>
@code {
private ProductModel product = new();
private async Task HandleSubmit()
{
await ProductService.CreateAsync(product);
Navigation.NavigateTo("/products");
}
}
```text
### Model with Validation Attributes
```csharp
public sealed class ProductModel
{
[Required(ErrorMessage = "Product name is required")]
[StringLength(200, MinimumLength = 1)]
public string Name { get; set; } = "";
[Range(0.01, 1_000_000, ErrorMessage = "Price must be between {1} and {2}")]
public decimal Price { get; set; }
[Required(ErrorMessage = "Category is required")]
public string Category { get; set; } = "";
}
```text
### EditForm with Enhanced Form Handling (.NET 8+)
Static SSR forms require `FormName` and use `[SupplyParameterFromForm]`:
```razor
@page "/products/create"
<EditForm Model="product" OnValidSubmit="HandleSubmit" FormName="create-product" Enhance>
<DataAnnotationsValidator />
<!-- form fields -->
<button type="submit">Create</button>
</EditForm>
@code {
[SupplyParameterFromForm]
private ProductModel product { get; set; } = new();
private async Task HandleSubmit()
{
await ProductService.CreateAsync(product);
Navigation.NavigateTo("/products");
}
}
```text
The `Enhance` attribute enables enhanced form handling -- the form submits via fetch and patches the DOM without a full
page reload.
**Gotcha:** `FormName` must be unique across all forms on the page. Duplicate `FormName` values cause ambiguous form
submission errors.
---
## QuickGrid
QuickGrid is a high-performance grid component built into Blazor (.NET 8+). It supports sorting, filtering, pagination,
and virtualization.
### Basic QuickGrid
```razor
@using Microsoft.AspNetCore.Components.QuickGrid
<QuickGrid Items="products">
<PropertyColumn Property="p => p.Name" Sortable="true" />
<PropertyColumn Property="p => p.Price" Format="C2" Sortable="true" />
<PropertyColumn Property="p => p.Category" Sortable="true" />
<TemplateColumn Title="Actions">
<button @onclick="() => Edit(context)">Edit</button>
</TemplateColumn>
</QuickGrid>
@code {
private IQueryable<Product> products = Enumerable.Empty<Product>().AsQueryable();
protected override async Task OnInitializedAsync()
{
var list = await ProductService.GetAllAsync();
products = list.AsQueryable();
}
private void Edit(Product product) => Navigation.NavigateTo($"/products/{product.Id}/edit");
}
```text
### QuickGrid with Pagination
```razor
<QuickGrid Items="products" Pagination="pagination">
<PropertyColumn Property="p => p.Name" Sortable="true" />
<PropertyColumn Property="p => p.Price" Format="C2" />
</QuickGrid>
<Paginator State="pagination" />
@code {
private PaginationState pagination = new() { ItemsPerPage = 20 };
private IQueryable<Product> products = default!;
}
```text
### QuickGrid with Virtualization
For large datasets, virtualization renders only visible rows:
```razor
<QuickGrid Items="products" Virtualize="true" ItemSize="50">
<PropertyColumn Property="p => p.Name" />
<PropertyColumn Property="p => p.Price" Format="C2" />
</QuickGrid>
```text
<!-- net11-preview -->
### QuickGrid OnRowClick (.NET 11 Preview)
.NET 11 adds `OnRowClick` to QuickGrid for row-level click handling without template columns:
```razor
<QuickGrid Items="products" OnRowClick="HandleRowClick">
<PropertyColumn Property="p => p.Name" />
<PropertyColumn Property="p => p.Price" Format="C2" />
</QuickGrid>
@code {
private void HandleRowClick(GridRowClickEventArgs<Product> args)
{
Navigation.NavigateTo($"/products/{args.Item.Id}");
}
}
```text
**Fallback (net10.0):** Use a `TemplateColumn` with a click handler or wrap each row in a clickable element.
Source:
[ASP.NET Core .NET 11 Preview - QuickGrid enhancements](https://learn.microsoft.com/en-us/aspnet/core/release-notes/aspnetcore-11.0)
---
<!-- net11-preview -->
## .NET 11 Preview Features
### EnvironmentBoundary Component
`EnvironmentBoundary` conditionally renders content based on the hosting environment (Development, Staging, Production):
```razor
<EnvironmentBoundary Include="Development">
<p>Debug panel -- only visible in Development</p>
<DebugToolbar />
</EnvironmentBoundary>
<EnvironmentBoundary Exclude="Production">
<p>Testing controls -- hidden in Production</p>
</EnvironmentBoundary>
```text
**Fallback (net10.0):** Inject `IWebHostEnvironment` and use conditional rendering in `@code`.
Source:
[ASP.NET Core .NET 11 Preview - EnvironmentBoundary](https://learn.microsoft.com/en-us/aspnet/core/release-notes/aspnetcore-11.0)
### Label and DisplayName Support
.NET 11 adds `[DisplayName]` support for input components, automatically generating `<label>` elements:
```razor
<EditForm Model="model" FormName="contact">
<!-- Automatically renders <label> from [DisplayName] -->
<InputText @bind-Value="model.FullName" />
<InputText @bind-Value="model.EmailAddress" />
</EditForm>
@code {
private ContactModel model = new();
}
// Model
public sealed class ContactModel
{
[DisplayName("Full Name")]
[Required]
public string FullName { get; set; } = "";
[DisplayName("Email Address")]
[EmailAddress]
public string EmailAddress { get; set; } = "";
}
```text
**Fallback (net10.0):** Add explicit `<label for="...">` elements manually.
Source:
[ASP.NET Core .NET 11 Preview - Label/DisplayName](https://learn.microsoft.com/en-us/aspnet/core/release-notes/aspnetcore-11.0)
### IHostedService in WebAssembly
.NET 11 allows `IHostedService` implementations to run in Blazor WebAssembly, enabling background tasks in the browser:
```csharp
// Register in WASM Program.cs
builder.Services.AddHostedService<DataSyncService>();
public sealed class DataSyncService : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
await SyncDataFromServer();
await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken);
}
}
}
```text
**Fallback (net10.0):** Use a `Timer` in a component or inject a singleton service that starts background work on first
use.
Source:
[ASP.NET Core .NET 11 Preview - IHostedService in WASM](https://learn.microsoft.com/en-us/aspnet/core/release-notes/aspnetcore-11.0)
<!-- net11-preview -->
### SignalR ConfigureConnection
.NET 11 adds `ConfigureConnection` to the Blazor Server circuit hub, allowing customization of the SignalR connection
(e.g., adding custom headers, configuring reconnection):
```csharp
// Program.cs
app.MapBlazorHub(options =>
{
options.ConfigureConnection = connection =>
{
connection.Metadata["tenant"] = "default";
};
});
```text
**Fallback (net10.0):** Use `IHubFilter` or middleware to inspect/modify connections at the hub level.
Source:
[ASP.NET Core .NET 11 Preview - SignalR ConfigureConnection](https://learn.microsoft.com/en-us/aspnet/core/release-notes/aspnetcore-11.0)
---
## Agent Gotchas
1. **Do not call JS interop in `OnInitializedAsync`.** The DOM is not available yet. Use
`OnAfterRenderAsync(firstRender: true)` for JS calls that need DOM elements.
2. **Do not forget `StateHasChanged()` after external state changes.** When state changes from a non-Blazor context
(timer, event handler, JS callback), call `StateHasChanged()` or `InvokeAsync(StateHasChanged)` to trigger re-render.
3. **Do not use `ProtectedBrowserStorage` during prerendering.** It throws because no interactive circuit exists yet.
Access it only in `OnAfterRenderAsync`.
4. **Do not forget `FormName` on Static SSR forms.** Without it, form submissions in Static SSR mode are not routed to
the correct handler.
5. **Do not dispose `DotNetObjectReference` before JS is done with it.** Premature disposal causes `JSException` when
JavaScript tries to invoke the callback. Dispose in `Dispose()` or `DisposeAsync()`.
6. **Do not assume Scoped services are per-request in Blazor Server.** Scoped services live for the entire circuit. Use
`OwningComponentBase<T>` when you need component-scoped service lifetimes.
---
## Prerequisites
- .NET 8.0+ (QuickGrid, enhanced form handling, cascading values with `IsFixed`)
- `Microsoft.AspNetCore.Components.QuickGrid` package for QuickGrid
- .NET 11 preview for EnvironmentBoundary, Label/DisplayName, QuickGrid OnRowClick, IHostedService in WASM
---
## Knowledge Sources
Blazor component patterns in this skill are grounded in guidance from:
- **Damian Edwards** -- Razor and Blazor component design patterns, render mode architecture, and performance best
practices. Principal architect on the ASP.NET team.
> These sources inform the patterns and rationale presented above. This skill does not claim to represent or speak for
> any individual.
---
## References
- [Blazor Component Lifecycle](https://learn.microsoft.com/en-us/aspnet/core/blazor/components/lifecycle?view=aspnetcore-10.0)
- [Blazor State Management](https://learn.microsoft.com/en-us/aspnet/core/blazor/state-management?view=aspnetcore-10.0)
- [Blazor JS Interop](https://learn.microsoft.com/en-us/aspnet/core/blazor/javascript-interoperability/?view=aspnetcore-10.0)
- [Blazor Forms and Validation](https://learn.microsoft.com/en-us/aspnet/core/blazor/forms/?view=aspnetcore-10.0)
- [QuickGrid Component](https://learn.microsoft.com/en-us/aspnet/core/blazor/components/quickgrid?view=aspnetcore-10.0)
- [Cascading Values and Parameters](https://learn.microsoft.com/en-us/aspnet/core/blazor/components/cascading-values-and-parameters?view=aspnetcore-10.0)
Weekly Installs
1
Repository
rudironsoni/dot…s-pluginFirst Seen
11 days ago
Security Audits
Installed on
amp1
cline1
opencode1
cursor1
kimi-cli1
codex1