
Blazor
- 23 installs
- 466 repo stars
- Updated July 25, 2026
- managedcode/dotnet-skills
Helps with ai & agent building tasks.
About
blazor is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- blazor
- AI & Agent Building
- AI-coding skill
Blazor by the numbers
- 23 all-time installs (skills.sh)
- +2 installs in the week ending Aug 2, 2026 (Skillselion tracking)
- Ranked #9,994 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 3, 2026 (Skillselion catalog sync)
npx skills add https://github.com/managedcode/dotnet-skills --skill blazorAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 23 |
|---|---|
| repo stars | ★ 466 |
| Last updated | July 25, 2026 |
| Repository | managedcode/dotnet-skills ↗ |
What it does
Helps with ai & agent building tasks.
Files
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 neededWorkflow
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)
Current Upstream Notes
- Treat
dotnet/aspnetcorev9.0.17as servicing. For new guidance, keep using the.NET 10Blazor docs and the imported official Blazor task skills for project creation, component authoring, user input, auth, data, JS interop, and prerendering. - When existing apps update servicing packages, recheck render-mode assumptions, SignalR circuit behavior, and any interactive-auto client/server service split.
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
1. Virtualize large lists:
<Virtualize Items="@products" Context="product">
<ProductCard Product="@product" />
</Virtualize>2. Use `@key` for list diffing:
@foreach (var item in items)
{
<ItemComponent @key="item.Id" Item="@item" />
}3. Debounce rapid events:
private Timer? _debounceTimer;
private void OnInput(ChangeEventArgs e)
{
_debounceTimer?.Dispose();
_debounceTimer = new Timer(_ => InvokeAsync(DoSearch), null, 300, Timeout.Infinite);
}4. 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
{
"version": "1.0.1",
"category": "Web",
"package_prefix": "Microsoft.AspNetCore.Components"
}
Blazor Anti-Patterns
Component Design Anti-Patterns
Monolithic Components
Problem: Creating large components that handle multiple concerns.
@* 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.
@* GOOD: Composed from smaller components *@
<ProductPage>
<ProductFilters />
<ProductGrid Products="@products" />
<CartSidebar />
</ProductPage>Parameter Drilling
Problem: Passing parameters through many component layers.
@* 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.
@* 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.
@* 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.
@* 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.
@* BAD: No indication this is required *@
@code {
[Parameter] public Product Product { get; set; } = default!;
}Solution: Use EditorRequired for mandatory parameters.
@* 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.
// 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.
// 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.
@* BAD: Never unsubscribes *@
@code {
protected override void OnInitialized()
{
CartService.OnChange += StateHasChanged;
}
}Solution: Implement IDisposable.
@* 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).
@* 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.
@* 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.
// 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.
// 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 HttpClientIgnoring Render Mode Boundaries
Problem: Assuming all components run in the same mode.
@* BAD: Child assumes parent's render mode *@
<InteractiveParent>
<StaticChild /> @* May not behave as expected *@
</InteractiveParent>Solution: Explicitly set render modes and understand boundaries.
@* 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.
// BAD: Only works on server
services.AddScoped<IDataService, ServerOnlyDataService>();Solution: Register environment-specific implementations.
// 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.
@* BAD: No key, poor diffing *@
@foreach (var item in items)
{
<ItemComponent Item="@item" />
}Solution: Use @key for efficient updates.
@* 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.
@* BAD: Renders all 10,000 items *@
@foreach (var item in allItems)
{
<ItemRow Item="@item" />
}Solution: Use Virtualize component.
@* 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.
@* BAD: Re-renders on every parent change *@
<ExpensiveComponent Data="@unchangingData" />Solution: Override ShouldRender for expensive components.
@* 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.
// BAD: Blocks the thread
protected override void OnInitialized()
{
var data = Http.GetFromJsonAsync<Data>("api/data").Result; // BLOCKS!
}Solution: Use proper async patterns.
// 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.
// BAD: Blocks in Server mode
var result = ((IJSInProcessRuntime)JS).Invoke<string>("getValue");Solution: Always use async JS interop.
// GOOD: Non-blocking
var result = await JS.InvokeAsync<string>("getValue");JS Interop During Prerendering
Problem: Calling JS during prerender when there's no browser.
// 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.
// 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.
// 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.
// 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.
// BAD: Serializes entire dataset
await JS.InvokeVoidAsync("processData", hugeDataSet);Solution: Use streaming or pass references.
// 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.
@* BAD: No validation *@
<EditForm Model="@model" OnSubmit="Submit">
<InputText @bind-Value="model.Email" />
<button type="submit">Submit</button>
</EditForm>Solution: Add validators.
@* 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.
@* BAD: Can submit multiple times *@
<button type="submit">Submit</button>Solution: Track and display submission state.
@* 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.
@* BAD: Unhandled exception crashes circuit *@
<RiskyComponent />Solution: Wrap risky components in ErrorBoundary.
@* 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.
// BAD: Silent failure
try
{
await SaveDataAsync();
}
catch
{
// Swallowed - user has no idea it failed
}Solution: Provide feedback and logging.
// 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.
@* BAD: Client-side only - easily bypassed *@
@if (isAdmin)
{
<AdminPanel />
}Solution: Always validate on server.
// 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.
@* BAD: API key visible in browser state *@
@code {
private string apiKey = "secret-api-key-12345";
}Solution: Keep secrets server-side only.
// 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.
// BAD: Direct use of user input
var userId = userIdFromClient;
var data = await Db.GetUserData(userId); // Can access any user's dataSolution: Validate against authenticated user.
// GOOD: Validate ownership
var authenticatedUserId = GetAuthenticatedUserId();
if (requestedUserId != authenticatedUserId)
{
throw new UnauthorizedAccessException();
}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)
@* 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)
@* 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.
@* 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:
<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.
@* 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.
@* 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;
}@* 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.
// 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;
}
}
}@* 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.
@code {
private int count = 0;
private string message = "";
private void Increment() => count++;
}Service-Based Shared State
For state shared across multiple components using DI.
// 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();
}@* 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.
// 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
}
}@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.
@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
@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.
// 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();
}@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.
// 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
@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
// Stream large data to JavaScript
using var streamRef = new DotNetStreamReference(stream: myLargeDataStream);
await JS.InvokeVoidAsync("receiveStream", streamRef);async function receiveStream(streamRef) {
const data = await streamRef.arrayBuffer();
// Process data
}Advanced Patterns
Dynamic Component Loading
<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.
@* StaticLayout.razor - No render mode *@
<header>
<nav>Static navigation</nav>
</header>
<main>
@Body
</main>
<footer>Static footer</footer>@* InteractiveDashboard.razor *@
@rendermode InteractiveServer
<div class="dashboard">
<RealTimeChart />
<LiveNotifications />
</div>Error Boundary Pattern
<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.
@* MainLayout.razor *@
<header>
<SectionOutlet SectionName="PageHeader" />
</header>
<main>@Body</main>
<aside>
<SectionOutlet SectionName="Sidebar" />
</aside>@* ProductPage.razor *@
<SectionContent SectionName="PageHeader">
<h1>Products</h1>
<SearchBar />
</SectionContent>
<SectionContent SectionName="Sidebar">
<CategoryFilter />
<PriceRangeFilter />
</SectionContent>
<ProductGrid Products="@products" />Render Optimization with ShouldRender
@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
<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;
}
}
}