--- name: dotnet-blazor-components description: Implements Blazor components. Lifecycle, state management, JS interop, EditForm, QuickGrid. license: MIT user-invocable: false --- # 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 ```csharp @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(); } } } ``` ### 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 @code { private ThemeSettings theme = new() { IsDarkMode = false, AccentColor = "#0078d4" }; } ``` ```razor @code { [CascadingParameter(Name = "AppTheme")] public ThemeSettings? Theme { get; set; } } ``` **Fixed cascading values (.NET 8+):** For values that never change after initial render, use `IsFixed="true"` to avoid re-render overhead: ```razor ``` ### Dependency Injection ```csharp // Register services in Program.cs builder.Services.AddScoped(); builder.Services.AddSingleton(); // Inject in components @inject IProductService ProductService @inject AppState State ``` **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` 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>("cart"); if (result.Success) { cartItems = result.Value!; } // Local storage (persists across sessions) await LocalStorage.SetAsync("preferences", userPrefs); } } ``` 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("localStorage.getItem", "key"); if (json is not null) { value = JsonSerializer.Deserialize(json, AppJsonContext.Default.UserPrefs); } ``` **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("getWindowWidth"); // With timeout (important for Server to avoid hanging circuits) var result = await JSRuntime.InvokeAsync( "expensiveOperation", TimeSpan.FromSeconds(10), inputData); ``` ### 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( "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(); } } ``` ```javascript // wwwroot/js/interop.js export function initialize(element) { // Set up the element } export function getValue(element) { return element.value; } ``` ### Calling .NET from JavaScript ```csharp // Instance method callback private DotNetObjectReference? dotNetRef; protected override void OnInitialized() { dotNetRef = DotNetObjectReference.Create(this); } [JSInvokable] public void OnJsEvent(string data) { message = data; StateHasChanged(); } public void Dispose() { dotNetRef?.Dispose(); } ``` ```javascript // Call .NET from JS export function registerCallback(dotNetRef) { document.addEventListener('custom-event', (e) => { dotNetRef.invokeMethodAsync('OnJsEvent', e.detail); }); } ``` ### 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
@code { private ProductModel product = new(); private async Task HandleSubmit() { await ProductService.CreateAsync(product); Navigation.NavigateTo("/products"); } } ``` ### 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; } = ""; } ``` ### EditForm with Enhanced Form Handling (.NET 8+) Static SSR forms require `FormName` and use `[SupplyParameterFromForm]`: ```razor @page "/products/create" @code { [SupplyParameterFromForm] private ProductModel product { get; set; } = new(); private async Task HandleSubmit() { await ProductService.CreateAsync(product); Navigation.NavigateTo("/products"); } } ``` 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 @code { private IQueryable products = Enumerable.Empty().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"); } ``` ### QuickGrid with Pagination ```razor @code { private PaginationState pagination = new() { ItemsPerPage = 20 }; private IQueryable products = default!; } ``` ### QuickGrid with Virtualization For large datasets, virtualization renders only visible rows: ```razor ``` ### QuickGrid OnRowClick (.NET 11 Preview) .NET 11 adds `OnRowClick` to QuickGrid for row-level click handling without template columns: ```razor @code { private void HandleRowClick(GridRowClickEventArgs args) { Navigation.NavigateTo($"/products/{args.Item.Id}"); } } ``` **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) --- ## .NET 11 Preview Features ### EnvironmentBoundary Component `EnvironmentBoundary` conditionally renders content based on the hosting environment (Development, Staging, Production): ```razor

Debug panel -- only visible in Development

Testing controls -- hidden in Production

``` **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 `