dotnet-blazor
Build and review Blazor applications across server, WebAssembly, web app, and hybrid scenarios with correct component design, state flow, rendering, and hosting choices.
Install
npx skills add https://github.com/Postpartum-genushyacinthus29/dotnet-skills/tree/main/skills/dotnet-blazor
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install postpartum-genushyacinthus29-dotnet-skills@llmmart
git clone https://github.com/Postpartum-genushyacinthus29/dotnet-skills.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole postpartum-genushyacinthus29/dotnet-skills collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
Blazor
Trigger On
- building interactive web UIs with C# instead of JavaScript
- choosing between Server, WebAssembly, or Auto render modes
- designing component hierarchies and state management
- handling prerendering and hydration
- integrating with JavaScript when necessary
Documentation
References
- patterns.md - Detailed component patterns, state management strategies, and JS interop techniques
- anti-patterns.md - Common Blazor mistakes and how to avoid them
Render Modes (.NET 8+)
| Mode | Where It Runs | Best For |
|---|---|---|
Static |
Server (no interactivity) | SEO pages, marketing content |
InteractiveServer |
Server via SignalR | Real-time apps, thin clients |
InteractiveWebAssembly |
Browser via WASM | Offline-capable, client-heavy |
InteractiveAuto |
Server first, then WASM | Best of both worlds |
Applying Render Modes
@* Per-component *@
@rendermode InteractiveServer
@* Or in App.razor for global *@
<Routes @rendermode="InteractiveAuto" />
InteractiveAuto Architecture
First Request:
Browser → Server (Interactive Server) → Fast response
Subsequent Requests:
Browser → WASM (downloaded in background) → No server needed
Workflow
Choose render mode based on requirements:
- Need SEO? Start with Static or prerendering
- Need real-time? Use InteractiveServer
- Need offline? Use InteractiveWebAssembly
- Want both? Use InteractiveAuto
Design components for reusability:
- Small, focused components
- Parameters for customization
- Events for communication
Handle state correctly:
- Component state lives in component
- Shared state via services (DI)
- Persist state across prerender with
[PersistentState]
Validate in both environments (for Auto mode)
Component Patterns
Basic Component
@* Counter.razor *@
<button @onclick="IncrementCount">
Clicked @count times
</button>
@code {
private int count = 0;
[Parameter]
public int InitialCount { get; set; } = 0;
protected override void OnInitialized()
{
count = InitialCount;
}
private void IncrementCount() => count++;
}
Parameter and Event Callbacks
@* Parent.razor *@
<ChildComponent Value="@value" ValueChanged="@OnValueChanged" />
@* ChildComponent.razor *@
@code {
[Parameter] public string Value { get; set; } = "";
[Parameter] public EventCallback<string> ValueChanged { get; set; }
private async Task UpdateValue(string newValue)
{
await ValueChanged.InvokeAsync(newValue);
}
}
State Persistence (.NET 8+)
@* Prevents double-fetch during prerender + hydration *@
@code {
[PersistentState]
public List<Product> Products { get; set; } = [];
protected override async Task OnInitializedAsync()
{
// Only fetches once, persisted across prerender
Products ??= await Http.GetFromJsonAsync<List<Product>>("api/products");
}
}
Data Access Pattern for Auto Mode
// Shared interface
public interface IProductService
{
Task<List<Product>> GetProductsAsync();
}
// Server implementation (direct DB access)
public class ServerProductService : IProductService
{
private readonly AppDbContext _db;
public async Task<List<Product>> GetProductsAsync()
=> await _db.Products.ToListAsync();
}
// Client implementation (HTTP call)
public class ClientProductService : IProductService
{
private readonly HttpClient _http;
public async Task<List<Product>> GetProductsAsync()
=> await _http.GetFromJsonAsync<List<Product>>("api/products");
}
// Registration
// Server: builder.Services.AddScoped<IProductService, ServerProductService>();
// Client: builder.Services.AddScoped<IProductService, ClientProductService>();
Anti-Patterns to Avoid
| Anti-Pattern | Why It's Bad | Better Approach |
|---|---|---|
| Large components | Hard to maintain, slow renders | Split into smaller components |
| Direct DB access in WASM | No DB in browser | Use HTTP API |
Ignoring ShouldRender |
Unnecessary re-renders | Override when needed |
| Sync JS interop in Server | Blocks SignalR circuit | Use IJSRuntime async |
| No error boundaries | One error crashes app | Use <ErrorBoundary> |
| Forgetting prerender state | Double API calls | Use [PersistentState] |
Performance Best Practices
Virtualize large lists:
<Virtualize Items="@products" Context="product"> <ProductCard Product="@product" /> </Virtualize>Use
@keyfor list diffing:@foreach (var item in items) { <ItemComponent @key="item.Id" Item="@item" /> }Debounce rapid events:
private Timer? _debounceTimer; private void OnInput(ChangeEventArgs e) { _debounceTimer?.Dispose(); _debounceTimer = new Timer(_ => InvokeAsync(DoSearch), null, 300, Timeout.Infinite); }Lazy load assemblies (WASM):
var assemblies = await LazyAssemblyLoader .LoadAssembliesAsync(["MyHeavyFeature.wasm"]);
JS Interop
Calling JavaScript from C#
@inject IJSRuntime JS
await JS.InvokeVoidAsync("alert", "Hello from Blazor!");
var result = await JS.InvokeAsync<string>("prompt", "Enter name:");
Calling C# from JavaScript
[JSInvokable]
public static string GetMessage() => "Hello from C#!";
DotNet.invokeMethodAsync('MyAssembly', 'GetMessage')
.then(result => console.log(result));
Deliver
- interactive Blazor components with appropriate render mode
- efficient state management and data flow
- proper handling of prerendering scenarios
- performant list rendering with virtualization
Validate
- components render correctly in chosen mode
- state persists correctly across prerender/hydration
- no unnecessary re-renders (check with browser tools)
- JS interop works in both Server and WASM
- error boundaries catch component failures
- Auto mode works in both environments
Files (dotnet-skills)
-
references
-
anti-patterns.md 13.5 KB
# Blazor Anti-Patterns ## Component Design Anti-Patterns ### Monolithic Components **Problem:** Creating large components that handle multiple concerns. ```razor @* BAD: One component doing everything *@ @code { private List<Product> products = []; private List<Category> categories = []; private Cart cart = new(); private User? user; private bool showFilters = false; private string searchTerm = ""; private decimal minPrice; private decimal maxPrice; // ... 500 more lines of mixed concerns } ``` **Solution:** Split into focused components with single responsibilities. ```razor @* GOOD: Composed from smaller components *@ <ProductPage> <ProductFilters /> <ProductGrid Products="@products" /> <CartSidebar /> </ProductPage> ``` ### Parameter Drilling **Problem:** Passing parameters through many component layers. ```razor @* BAD: Drilling user through multiple levels *@ <Layout User="@user"> <Sidebar User="@user"> <UserMenu User="@user"> <Avatar User="@user" /> </UserMenu> </Sidebar> </Layout> ``` **Solution:** Use cascading values for widely-needed data. ```razor @* GOOD: Cascading value *@ <CascadingValue Value="@user"> <Layout> <Sidebar> <UserMenu /> @* Accesses user via [CascadingParameter] *@ </Sidebar> </Layout> </CascadingValue> ``` ### Mutable Parameter Objects **Problem:** Modifying parameter objects directly, bypassing change detection. ```razor @* BAD: Mutating parameter object *@ @code { [Parameter] public Product Product { get; set; } = default!; private void UpdatePrice() { Product.Price = 99.99m; // Parent won't know about this change } } ``` **Solution:** Use events to notify parent of changes. ```razor @* GOOD: Notify parent via callback *@ @code { [Parameter] public Product Product { get; set; } = default!; [Parameter] public EventCallback<Product> ProductChanged { get; set; } private async Task UpdatePrice() { var updated = Product with { Price = 99.99m }; await ProductChanged.InvokeAsync(updated); } } ``` ### Missing EditorRequired **Problem:** Forgetting to mark required parameters, leading to runtime errors. ```razor @* BAD: No indication this is required *@ @code { [Parameter] public Product Product { get; set; } = default!; } ``` **Solution:** Use EditorRequired for mandatory parameters. ```razor @* GOOD: Compiler warns if not provided *@ @code { [Parameter, EditorRequired] public Product Product { get; set; } = default!; } ``` ## State Management Anti-Patterns ### Global Static State **Problem:** Using static fields for state, causing cross-user data leakage in Server mode. ```csharp // BAD: Static state is shared across ALL users in Server mode public static class AppState { public static User? CurrentUser { get; set; } public static List<CartItem> Cart { get; } = []; } ``` **Solution:** Use scoped services. ```csharp // GOOD: Scoped per-circuit in Server mode public class AppState { public User? CurrentUser { get; set; } public List<CartItem> Cart { get; } = []; } // Registration services.AddScoped<AppState>(); ``` ### Forgetting to Dispose Event Subscriptions **Problem:** Memory leaks from event subscriptions. ```razor @* BAD: Never unsubscribes *@ @code { protected override void OnInitialized() { CartService.OnChange += StateHasChanged; } } ``` **Solution:** Implement IDisposable. ```razor @* GOOD: Clean up subscriptions *@ @implements IDisposable @code { protected override void OnInitialized() { CartService.OnChange += StateHasChanged; } public void Dispose() { CartService.OnChange -= StateHasChanged; } } ``` ### Double Data Fetching with Prerendering **Problem:** Fetching data twice (once during prerender, once during interactive). ```razor @* BAD: Fetches twice *@ @code { private List<Product> products = []; protected override async Task OnInitializedAsync() { products = await Http.GetFromJsonAsync<List<Product>>("api/products") ?? []; } } ``` **Solution:** Use PersistentState or PersistentComponentState. ```razor @* GOOD: Data persists across prerender *@ @code { [PersistentState] public List<Product> Products { get; set; } = []; protected override async Task OnInitializedAsync() { if (Products.Count == 0) { Products = await Http.GetFromJsonAsync<List<Product>>("api/products") ?? []; } } } ``` ## Render Mode Anti-Patterns ### Direct Database Access in WASM Components **Problem:** Trying to use DbContext in WebAssembly. ```csharp // BAD: DbContext doesn't work in browser @inject AppDbContext Db @code { protected override async Task OnInitializedAsync() { products = await Db.Products.ToListAsync(); // Will fail in WASM } } ``` **Solution:** Use HTTP API abstraction. ```csharp // GOOD: Works in both Server and WASM @inject IProductService ProductService @code { protected override async Task OnInitializedAsync() { products = await ProductService.GetProductsAsync(); } } // Server implementation uses DbContext // Client implementation uses HttpClient ``` ### Ignoring Render Mode Boundaries **Problem:** Assuming all components run in the same mode. ```razor @* BAD: Child assumes parent's render mode *@ <InteractiveParent> <StaticChild /> @* May not behave as expected *@ </InteractiveParent> ``` **Solution:** Explicitly set render modes and understand boundaries. ```razor @* GOOD: Explicit render mode *@ <div> <StaticHeader /> <InteractiveContent @rendermode="InteractiveServer" /> <StaticFooter /> </div> ``` ### Auto Mode Without Dual Implementation **Problem:** Using Auto render mode without supporting both environments. ```csharp // BAD: Only works on server services.AddScoped<IDataService, ServerOnlyDataService>(); ``` **Solution:** Register environment-specific implementations. ```csharp // Server project services.AddScoped<IDataService, ServerDataService>(); // Client project services.AddScoped<IDataService, ClientDataService>(); ``` ## Performance Anti-Patterns ### Missing @key on Lists **Problem:** Blazor recreates all list items on changes. ```razor @* BAD: No key, poor diffing *@ @foreach (var item in items) { <ItemComponent Item="@item" /> } ``` **Solution:** Use @key for efficient updates. ```razor @* GOOD: Efficient list diffing *@ @foreach (var item in items) { <ItemComponent @key="item.Id" Item="@item" /> } ``` ### Rendering Large Lists Without Virtualization **Problem:** Rendering thousands of items at once. ```razor @* BAD: Renders all 10,000 items *@ @foreach (var item in allItems) { <ItemRow Item="@item" /> } ``` **Solution:** Use Virtualize component. ```razor @* GOOD: Only renders visible items *@ <Virtualize Items="@allItems" Context="item"> <ItemRow Item="@item" /> </Virtualize> ``` ### Unnecessary Re-renders **Problem:** Components re-render when they don't need to. ```razor @* BAD: Re-renders on every parent change *@ <ExpensiveComponent Data="@unchangingData" /> ``` **Solution:** Override ShouldRender for expensive components. ```razor @* GOOD: Controlled re-rendering *@ @code { private object? previousData; [Parameter] public object? Data { get; set; } protected override bool ShouldRender() { var shouldRender = !ReferenceEquals(Data, previousData); previousData = Data; return shouldRender; } } ``` ### Blocking Async Operations **Problem:** Using synchronous waits that block the render thread. ```csharp // BAD: Blocks the thread protected override void OnInitialized() { var data = Http.GetFromJsonAsync<Data>("api/data").Result; // BLOCKS! } ``` **Solution:** Use proper async patterns. ```csharp // GOOD: Non-blocking protected override async Task OnInitializedAsync() { var data = await Http.GetFromJsonAsync<Data>("api/data"); } ``` ## JavaScript Interop Anti-Patterns ### Synchronous JS Calls in Server Mode **Problem:** Synchronous JS interop blocks the SignalR circuit. ```csharp // BAD: Blocks in Server mode var result = ((IJSInProcessRuntime)JS).Invoke<string>("getValue"); ``` **Solution:** Always use async JS interop. ```csharp // GOOD: Non-blocking var result = await JS.InvokeAsync<string>("getValue"); ``` ### JS Interop During Prerendering **Problem:** Calling JS during prerender when there's no browser. ```csharp // BAD: Fails during prerender protected override async Task OnInitializedAsync() { await JS.InvokeVoidAsync("initializeMap"); // No JS runtime during prerender } ``` **Solution:** Call JS only after first interactive render. ```csharp // GOOD: Only when interactive protected override async Task OnAfterRenderAsync(bool firstRender) { if (firstRender) { await JS.InvokeVoidAsync("initializeMap"); } } ``` ### Not Disposing JS Object References **Problem:** Memory leaks from JS object references. ```csharp // BAD: Never disposed private IJSObjectReference? module; protected override async Task OnAfterRenderAsync(bool firstRender) { if (firstRender) { module = await JS.InvokeAsync<IJSObjectReference>("import", "./module.js"); } } ``` **Solution:** Implement IAsyncDisposable. ```csharp // GOOD: Proper cleanup @implements IAsyncDisposable private IJSObjectReference? module; public async ValueTask DisposeAsync() { if (module is not null) { await module.DisposeAsync(); } } ``` ### Large Data in JS Interop **Problem:** Passing large objects through JS interop serialization. ```csharp // BAD: Serializes entire dataset await JS.InvokeVoidAsync("processData", hugeDataSet); ``` **Solution:** Use streaming or pass references. ```csharp // GOOD: Stream large data using var streamRef = new DotNetStreamReference(dataStream); await JS.InvokeVoidAsync("processStream", streamRef); ``` ## Form Handling Anti-Patterns ### Missing Validation **Problem:** Forms without proper validation. ```razor @* BAD: No validation *@ <EditForm Model="@model" OnSubmit="Submit"> <InputText @bind-Value="model.Email" /> <button type="submit">Submit</button> </EditForm> ``` **Solution:** Add validators. ```razor @* GOOD: With validation *@ <EditForm Model="@model" OnValidSubmit="Submit"> <DataAnnotationsValidator /> <ValidationSummary /> <InputText @bind-Value="model.Email" /> <ValidationMessage For="() => model.Email" /> <button type="submit">Submit</button> </EditForm> ``` ### Not Handling Form Submission State **Problem:** Double submissions and no loading indication. ```razor @* BAD: Can submit multiple times *@ <button type="submit">Submit</button> ``` **Solution:** Track and display submission state. ```razor @* GOOD: Prevents double submit, shows status *@ <button type="submit" disabled="@isSubmitting"> @(isSubmitting ? "Submitting..." : "Submit") </button> @code { private bool isSubmitting = false; private async Task Submit() { isSubmitting = true; try { await SubmitFormAsync(); } finally { isSubmitting = false; } } } ``` ## Error Handling Anti-Patterns ### No Error Boundaries **Problem:** One component error crashes the whole application. ```razor @* BAD: Unhandled exception crashes circuit *@ <RiskyComponent /> ``` **Solution:** Wrap risky components in ErrorBoundary. ```razor @* GOOD: Contained errors *@ <ErrorBoundary> <ChildContent> <RiskyComponent /> </ChildContent> <ErrorContent Context="ex"> <p>Something went wrong: @ex.Message</p> </ErrorContent> </ErrorBoundary> ``` ### Swallowing Exceptions **Problem:** Catching exceptions without proper handling. ```csharp // BAD: Silent failure try { await SaveDataAsync(); } catch { // Swallowed - user has no idea it failed } ``` **Solution:** Provide feedback and logging. ```csharp // GOOD: User feedback and logging try { await SaveDataAsync(); message = "Saved successfully"; } catch (Exception ex) { Logger.LogError(ex, "Failed to save data"); errorMessage = "Failed to save. Please try again."; } ``` ## Security Anti-Patterns ### Client-Side Authorization Only **Problem:** Relying solely on client-side security checks. ```razor @* BAD: Client-side only - easily bypassed *@ @if (isAdmin) { <AdminPanel /> } ``` **Solution:** Always validate on server. ```csharp // GOOD: Server-side authorization [Authorize(Roles = "Admin")] public class AdminController : ControllerBase { // Server validates every request } ``` ### Exposing Sensitive Data in Component State **Problem:** Keeping secrets in component state visible to users. ```razor @* BAD: API key visible in browser state *@ @code { private string apiKey = "secret-api-key-12345"; } ``` **Solution:** Keep secrets server-side only. ```csharp // GOOD: Server-side service holds secrets public class SecureService { private readonly string _apiKey; public SecureService(IConfiguration config) { _apiKey = config["ApiKey"]!; } public async Task CallApiAsync() { // Uses _apiKey internally, never exposed to client } } ``` ### Trusting Client Input **Problem:** Using client input without validation. ```csharp // BAD: Direct use of user input var userId = userIdFromClient; var data = await Db.GetUserData(userId); // Can access any user's data ``` **Solution:** Validate against authenticated user. ```csharp // GOOD: Validate ownership var authenticatedUserId = GetAuthenticatedUserId(); if (requestedUserId != authenticatedUserId) { throw new UnauthorizedAccessException(); } ``` -
patterns.md 15.6 KB
# Blazor Component Patterns ## Component Design Patterns ### Smart vs Presentational Components Separate concerns by distinguishing between components that manage data and those that display it. **Presentational Component (Dumb)** ```razor @* ProductCard.razor - Only displays data *@ <div class="product-card"> <img src="@Product.ImageUrl" alt="@Product.Name" /> <h3>@Product.Name</h3> <p>@Product.Price.ToString("C")</p> <button @onclick="OnAddToCart">Add to Cart</button> </div> @code { [Parameter, EditorRequired] public Product Product { get; set; } = default!; [Parameter] public EventCallback OnAddToCart { get; set; } } ``` **Smart Component (Container)** ```razor @* ProductList.razor - Manages data and state *@ @inject IProductService ProductService @inject ICartService CartService <div class="product-list"> @foreach (var product in products) { <ProductCard Product="@product" OnAddToCart="() => AddToCart(product)" /> } </div> @code { private List<Product> products = []; protected override async Task OnInitializedAsync() { products = await ProductService.GetProductsAsync(); } private async Task AddToCart(Product product) { await CartService.AddAsync(product); } } ``` ### Templated Components Allow consumers to customize rendering with render fragments. ```razor @* DataGrid.razor *@ @typeparam TItem <table> <thead> <tr>@HeaderTemplate</tr> </thead> <tbody> @foreach (var item in Items) { <tr>@RowTemplate(item)</tr> } </tbody> </table> @code { [Parameter, EditorRequired] public IEnumerable<TItem> Items { get; set; } = []; [Parameter, EditorRequired] public RenderFragment HeaderTemplate { get; set; } = default!; [Parameter, EditorRequired] public RenderFragment<TItem> RowTemplate { get; set; } = default!; } ``` **Usage:** ```razor <DataGrid Items="@products"> <HeaderTemplate> <th>Name</th> <th>Price</th> </HeaderTemplate> <RowTemplate Context="product"> <td>@product.Name</td> <td>@product.Price.ToString("C")</td> </RowTemplate> </DataGrid> ``` ### Generic Components Create type-safe reusable components. ```razor @* SelectList.razor *@ @typeparam TItem @typeparam TValue <select @onchange="OnSelectionChanged"> @foreach (var item in Items) { <option value="@ValueSelector(item)" selected="@(EqualityComparer<TValue>.Default.Equals(ValueSelector(item), SelectedValue))"> @DisplaySelector(item) </option> } </select> @code { [Parameter, EditorRequired] public IEnumerable<TItem> Items { get; set; } = []; [Parameter, EditorRequired] public Func<TItem, TValue> ValueSelector { get; set; } = default!; [Parameter, EditorRequired] public Func<TItem, string> DisplaySelector { get; set; } = default!; [Parameter] public TValue? SelectedValue { get; set; } [Parameter] public EventCallback<TValue> SelectedValueChanged { get; set; } private async Task OnSelectionChanged(ChangeEventArgs e) { var value = (TValue)Convert.ChangeType(e.Value, typeof(TValue))!; await SelectedValueChanged.InvokeAsync(value); } } ``` ### Cascading Values Pattern Share data down the component tree without explicit parameter passing. ```razor @* App.razor or Layout *@ <CascadingValue Value="@theme" Name="AppTheme"> <CascadingValue Value="@currentUser"> @Body </CascadingValue> </CascadingValue> @code { private Theme theme = new() { IsDarkMode = false }; private User? currentUser; } ``` ```razor @* Any nested component *@ @code { [CascadingParameter(Name = "AppTheme")] public Theme Theme { get; set; } = default!; [CascadingParameter] public User? CurrentUser { get; set; } } ``` ### Component Inheritance Share logic across related components. ```csharp // BaseFormComponent.cs public abstract class BaseFormComponent<TModel> : ComponentBase { [Parameter] public TModel? Model { get; set; } [Parameter] public EventCallback<TModel> OnSubmit { get; set; } protected bool IsSubmitting { get; set; } protected string? ErrorMessage { get; set; } protected async Task HandleSubmit() { IsSubmitting = true; ErrorMessage = null; try { await OnSubmit.InvokeAsync(Model); } catch (Exception ex) { ErrorMessage = ex.Message; } finally { IsSubmitting = false; } } } ``` ```razor @* ProductForm.razor *@ @inherits BaseFormComponent<Product> <EditForm Model="@Model" OnValidSubmit="HandleSubmit"> <DataAnnotationsValidator /> @* Form fields *@ <button type="submit" disabled="@IsSubmitting">Save</button> @if (ErrorMessage is not null) { <p class="error">@ErrorMessage</p> } </EditForm> ``` ## State Management Patterns ### Component-Level State For isolated, component-specific state. ```razor @code { private int count = 0; private string message = ""; private void Increment() => count++; } ``` ### Service-Based Shared State For state shared across multiple components using DI. ```csharp // CartState.cs public class CartState { private readonly List<CartItem> _items = []; public IReadOnlyList<CartItem> Items => _items.AsReadOnly(); public decimal Total => _items.Sum(i => i.Price * i.Quantity); public event Action? OnChange; public void AddItem(Product product, int quantity = 1) { var existing = _items.FirstOrDefault(i => i.ProductId == product.Id); if (existing is not null) { existing.Quantity += quantity; } else { _items.Add(new CartItem(product.Id, product.Name, product.Price, quantity)); } NotifyStateChanged(); } public void RemoveItem(int productId) { _items.RemoveAll(i => i.ProductId == productId); NotifyStateChanged(); } private void NotifyStateChanged() => OnChange?.Invoke(); } ``` ```razor @* CartIcon.razor *@ @inject CartState Cart @implements IDisposable <span class="cart-icon"> Cart (@Cart.Items.Count) </span> @code { protected override void OnInitialized() { Cart.OnChange += StateHasChanged; } public void Dispose() { Cart.OnChange -= StateHasChanged; } } ``` ### Fluxor Pattern (Redux-like) For complex applications needing predictable state management. ```csharp // State public record CounterState(int Count); // Actions public record IncrementAction; public record DecrementAction; public record SetCountAction(int Value); // Reducer public static class CounterReducers { [ReducerMethod] public static CounterState OnIncrement(CounterState state, IncrementAction action) => state with { Count = state.Count + 1 }; [ReducerMethod] public static CounterState OnDecrement(CounterState state, DecrementAction action) => state with { Count = state.Count - 1 }; [ReducerMethod] public static CounterState OnSetCount(CounterState state, SetCountAction action) => state with { Count = action.Value }; } // Effects (side effects) public class CounterEffects { [EffectMethod] public async Task HandleSetCountAsync(SetCountAction action, IDispatcher dispatcher) { await Task.Delay(100); // Simulate async work // Dispatch additional actions if needed } } ``` ```razor @inject IState<CounterState> CounterState @inject IDispatcher Dispatcher <p>Count: @CounterState.Value.Count</p> <button @onclick="Increment">+</button> @code { private void Increment() => Dispatcher.Dispatch(new IncrementAction()); } ``` ### Persistent State (Prerendering) Handle state that must survive the prerender-to-interactive transition. ```razor @inject PersistentComponentState ApplicationState @code { private List<Product>? products; private PersistingComponentStateSubscription persistingSubscription; protected override async Task OnInitializedAsync() { persistingSubscription = ApplicationState.RegisterOnPersisting(PersistData); if (!ApplicationState.TryTakeFromJson<List<Product>>("products", out var restored)) { products = await FetchProducts(); } else { products = restored; } } private Task PersistData() { ApplicationState.PersistAsJson("products", products); return Task.CompletedTask; } public void Dispose() { persistingSubscription.Dispose(); } } ``` ### .NET 8+ Simplified Persistent State ```razor @code { [PersistentState] public List<Product> Products { get; set; } = []; protected override async Task OnInitializedAsync() { if (Products.Count == 0) { Products = await Http.GetFromJsonAsync<List<Product>>("api/products") ?? []; } } } ``` ## JavaScript Interop Patterns ### Module Isolation Encapsulate JS code in ES6 modules for better organization. ```javascript // wwwroot/js/map.js export function initializeMap(elementId, options) { const map = new MapLibrary(document.getElementById(elementId), options); return DotNet.createJSObjectReference(map); } export function setMarker(map, lat, lng) { map.addMarker({ lat, lng }); } export function dispose(map) { map.destroy(); } ``` ```razor @inject IJSRuntime JS @implements IAsyncDisposable <div id="map-container"></div> @code { private IJSObjectReference? module; private IJSObjectReference? mapInstance; protected override async Task OnAfterRenderAsync(bool firstRender) { if (firstRender) { module = await JS.InvokeAsync<IJSObjectReference>( "import", "./js/map.js"); mapInstance = await module.InvokeAsync<IJSObjectReference>( "initializeMap", "map-container", new { zoom = 10 }); } } private async Task AddMarker(double lat, double lng) { if (module is not null && mapInstance is not null) { await module.InvokeVoidAsync("setMarker", mapInstance, lat, lng); } } public async ValueTask DisposeAsync() { if (module is not null) { if (mapInstance is not null) { await module.InvokeVoidAsync("dispose", mapInstance); } await module.DisposeAsync(); } } } ``` ### JS Interop Abstraction Service Wrap JS interop in a typed service for better testability. ```csharp // ILocalStorage.cs public interface ILocalStorage { Task<T?> GetItemAsync<T>(string key); Task SetItemAsync<T>(string key, T value); Task RemoveItemAsync(string key); } // LocalStorageService.cs public class LocalStorageService : ILocalStorage { private readonly IJSRuntime _js; public LocalStorageService(IJSRuntime js) => _js = js; public async Task<T?> GetItemAsync<T>(string key) { var json = await _js.InvokeAsync<string?>("localStorage.getItem", key); return json is null ? default : JsonSerializer.Deserialize<T>(json); } public async Task SetItemAsync<T>(string key, T value) { var json = JsonSerializer.Serialize(value); await _js.InvokeVoidAsync("localStorage.setItem", key, json); } public async Task RemoveItemAsync(string key) { await _js.InvokeVoidAsync("localStorage.removeItem", key); } } ``` ### Handling JS Interop in Prerendering ```razor @inject IJSRuntime JS @code { private bool isInteractive = false; protected override async Task OnAfterRenderAsync(bool firstRender) { if (firstRender) { isInteractive = true; StateHasChanged(); // Safe to call JS here await JS.InvokeVoidAsync("console.log", "Component is interactive"); } } } ``` ### .NET to JS Streaming ```csharp // Stream large data to JavaScript using var streamRef = new DotNetStreamReference(stream: myLargeDataStream); await JS.InvokeVoidAsync("receiveStream", streamRef); ``` ```javascript async function receiveStream(streamRef) { const data = await streamRef.arrayBuffer(); // Process data } ``` ## Advanced Patterns ### Dynamic Component Loading ```razor <DynamicComponent Type="@componentType" Parameters="@parameters" /> @code { private Type? componentType; private Dictionary<string, object>? parameters; private void LoadComponent(string name) { componentType = name switch { "chart" => typeof(ChartComponent), "table" => typeof(TableComponent), _ => typeof(PlaceholderComponent) }; parameters = new Dictionary<string, object> { { "Data", currentData } }; } } ``` ### Render Mode Boundary Pattern Isolate interactive components from static content. ```razor @* StaticLayout.razor - No render mode *@ <header> <nav>Static navigation</nav> </header> <main> @Body </main> <footer>Static footer</footer> ``` ```razor @* InteractiveDashboard.razor *@ @rendermode InteractiveServer <div class="dashboard"> <RealTimeChart /> <LiveNotifications /> </div> ``` ### Error Boundary Pattern ```razor <ErrorBoundary @ref="errorBoundary"> <ChildContent> <RiskyComponent /> </ChildContent> <ErrorContent Context="exception"> <div class="error-panel"> <h3>Something went wrong</h3> <p>@exception.Message</p> <button @onclick="Recover">Try Again</button> </div> </ErrorContent> </ErrorBoundary> @code { private ErrorBoundary? errorBoundary; private void Recover() { errorBoundary?.Recover(); } } ``` ### Section Pattern (.NET 8+) Define content slots that can be filled from nested components. ```razor @* MainLayout.razor *@ <header> <SectionOutlet SectionName="PageHeader" /> </header> <main>@Body</main> <aside> <SectionOutlet SectionName="Sidebar" /> </aside> ``` ```razor @* ProductPage.razor *@ <SectionContent SectionName="PageHeader"> <h1>Products</h1> <SearchBar /> </SectionContent> <SectionContent SectionName="Sidebar"> <CategoryFilter /> <PriceRangeFilter /> </SectionContent> <ProductGrid Products="@products" /> ``` ### Render Optimization with ShouldRender ```razor @code { private string? previousValue; [Parameter] public string? Value { get; set; } protected override bool ShouldRender() { // Only re-render if Value actually changed var shouldRender = Value != previousValue; previousValue = Value; return shouldRender; } } ``` ### Form Validation Pattern ```razor <EditForm Model="@model" OnValidSubmit="HandleSubmit" FormName="ProductForm"> <DataAnnotationsValidator /> <ValidationSummary /> <div class="form-group"> <label for="name">Name</label> <InputText id="name" @bind-Value="model.Name" class="form-control" /> <ValidationMessage For="() => model.Name" /> </div> <div class="form-group"> <label for="price">Price</label> <InputNumber id="price" @bind-Value="model.Price" class="form-control" /> <ValidationMessage For="() => model.Price" /> </div> <button type="submit" disabled="@isSubmitting"> @(isSubmitting ? "Saving..." : "Save") </button> </EditForm> @code { [SupplyParameterFromForm] private ProductModel model { get; set; } = new(); private bool isSubmitting = false; private async Task HandleSubmit() { isSubmitting = true; try { await ProductService.SaveAsync(model); } finally { isSubmitting = false; } } } ```
-
-
SKILL.md 7 KB
--- name: dotnet-blazor version: "1.0.0" category: "Web" description: "Build and review Blazor applications across server, WebAssembly, web app, and hybrid scenarios with correct component design, state flow, rendering, and hosting choices." compatibility: "Requires Blazor project (.NET 6+, preferably .NET 8+ for unified model)." --- # Blazor ## Trigger On - building interactive web UIs with C# instead of JavaScript - choosing between Server, WebAssembly, or Auto render modes - designing component hierarchies and state management - handling prerendering and hydration - integrating with JavaScript when necessary ## Documentation - [Blazor Overview](https://learn.microsoft.com/en-us/aspnet/core/blazor/?view=aspnetcore-10.0) - [Render Modes](https://learn.microsoft.com/en-us/aspnet/core/blazor/components/render-modes?view=aspnetcore-10.0) - [Performance Best Practices](https://learn.microsoft.com/en-us/aspnet/core/blazor/performance?view=aspnetcore-10.0) - [State Management](https://learn.microsoft.com/en-us/aspnet/core/blazor/state-management?view=aspnetcore-10.0) - [JS Interop](https://learn.microsoft.com/en-us/aspnet/core/blazor/javascript-interoperability/?view=aspnetcore-10.0) ### References - [patterns.md](references/patterns.md) - Detailed component patterns, state management strategies, and JS interop techniques - [anti-patterns.md](references/anti-patterns.md) - Common Blazor mistakes and how to avoid them ## Render Modes (.NET 8+) | Mode | Where It Runs | Best For | |------|---------------|----------| | `Static` | Server (no interactivity) | SEO pages, marketing content | | `InteractiveServer` | Server via SignalR | Real-time apps, thin clients | | `InteractiveWebAssembly` | Browser via WASM | Offline-capable, client-heavy | | `InteractiveAuto` | Server first, then WASM | Best of both worlds | ### Applying Render Modes ```razor @* Per-component *@ @rendermode InteractiveServer @* Or in App.razor for global *@ <Routes @rendermode="InteractiveAuto" /> ``` ### InteractiveAuto Architecture ``` First Request: Browser → Server (Interactive Server) → Fast response Subsequent Requests: Browser → WASM (downloaded in background) → No server needed ``` ## Workflow 1. **Choose render mode based on requirements:** - Need SEO? Start with Static or prerendering - Need real-time? Use InteractiveServer - Need offline? Use InteractiveWebAssembly - Want both? Use InteractiveAuto 2. **Design components for reusability:** - Small, focused components - Parameters for customization - Events for communication 3. **Handle state correctly:** - Component state lives in component - Shared state via services (DI) - Persist state across prerender with `[PersistentState]` 4. **Validate in both environments** (for Auto mode) ## Component Patterns ### Basic Component ```razor @* Counter.razor *@ <button @onclick="IncrementCount"> Clicked @count times </button> @code { private int count = 0; [Parameter] public int InitialCount { get; set; } = 0; protected override void OnInitialized() { count = InitialCount; } private void IncrementCount() => count++; } ``` ### Parameter and Event Callbacks ```razor @* Parent.razor *@ <ChildComponent Value="@value" ValueChanged="@OnValueChanged" /> @* ChildComponent.razor *@ @code { [Parameter] public string Value { get; set; } = ""; [Parameter] public EventCallback<string> ValueChanged { get; set; } private async Task UpdateValue(string newValue) { await ValueChanged.InvokeAsync(newValue); } } ``` ### State Persistence (.NET 8+) ```razor @* Prevents double-fetch during prerender + hydration *@ @code { [PersistentState] public List<Product> Products { get; set; } = []; protected override async Task OnInitializedAsync() { // Only fetches once, persisted across prerender Products ??= await Http.GetFromJsonAsync<List<Product>>("api/products"); } } ``` ## Data Access Pattern for Auto Mode ```csharp // Shared interface public interface IProductService { Task<List<Product>> GetProductsAsync(); } // Server implementation (direct DB access) public class ServerProductService : IProductService { private readonly AppDbContext _db; public async Task<List<Product>> GetProductsAsync() => await _db.Products.ToListAsync(); } // Client implementation (HTTP call) public class ClientProductService : IProductService { private readonly HttpClient _http; public async Task<List<Product>> GetProductsAsync() => await _http.GetFromJsonAsync<List<Product>>("api/products"); } // Registration // Server: builder.Services.AddScoped<IProductService, ServerProductService>(); // Client: builder.Services.AddScoped<IProductService, ClientProductService>(); ``` ## Anti-Patterns to Avoid | Anti-Pattern | Why It's Bad | Better Approach | |--------------|--------------|-----------------| | Large components | Hard to maintain, slow renders | Split into smaller components | | Direct DB access in WASM | No DB in browser | Use HTTP API | | Ignoring `ShouldRender` | Unnecessary re-renders | Override when needed | | Sync JS interop in Server | Blocks SignalR circuit | Use `IJSRuntime` async | | No error boundaries | One error crashes app | Use `<ErrorBoundary>` | | Forgetting prerender state | Double API calls | Use `[PersistentState]` | ## Performance Best Practices 1. **Virtualize large lists:** ```razor <Virtualize Items="@products" Context="product"> <ProductCard Product="@product" /> </Virtualize> ``` 2. **Use `@key` for list diffing:** ```razor @foreach (var item in items) { <ItemComponent @key="item.Id" Item="@item" /> } ``` 3. **Debounce rapid events:** ```csharp private Timer? _debounceTimer; private void OnInput(ChangeEventArgs e) { _debounceTimer?.Dispose(); _debounceTimer = new Timer(_ => InvokeAsync(DoSearch), null, 300, Timeout.Infinite); } ``` 4. **Lazy load assemblies (WASM):** ```csharp var assemblies = await LazyAssemblyLoader .LoadAssembliesAsync(["MyHeavyFeature.wasm"]); ``` ## JS Interop ### Calling JavaScript from C# ```csharp @inject IJSRuntime JS await JS.InvokeVoidAsync("alert", "Hello from Blazor!"); var result = await JS.InvokeAsync<string>("prompt", "Enter name:"); ``` ### Calling C# from JavaScript ```csharp [JSInvokable] public static string GetMessage() => "Hello from C#!"; ``` ```javascript DotNet.invokeMethodAsync('MyAssembly', 'GetMessage') .then(result => console.log(result)); ``` ## Deliver - interactive Blazor components with appropriate render mode - efficient state management and data flow - proper handling of prerendering scenarios - performant list rendering with virtualization ## Validate - components render correctly in chosen mode - state persists correctly across prerender/hydration - no unnecessary re-renders (check with browser tools) - JS interop works in both Server and WASM - error boundaries catch component failures - Auto mode works in both environments
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.