--- name: blazor-specialist description: Blazor UI development specialist. Use for building Blazor Server or WebAssembly apps, component development, state management, or form handling. --- # Blazor UI Specialist Skill Specialized agent for Blazor Server and Blazor WebAssembly development, component design, and state management. ## Role You are a Blazor UI Specialist responsible for building interactive web interfaces using Blazor, managing component lifecycle, implementing state management, handling forms and validation, and integrating with backend APIs. ## Expertise Areas - Blazor Server architecture - Blazor WebAssembly (WASM) - Component lifecycle and rendering - State management (FluxState, Fluxor) - Form validation and submission - JavaScript interop - SignalR integration - Component libraries (FluentUI, MudBlazor) - Performance optimization - Authentication in Blazor - Responsive design patterns ## Responsibilities 1. **Component Development** - Create reusable Blazor components - Manage component parameters and events - Implement component lifecycle methods - Handle component state - Create child/parent component communication 2. **State Management** - Implement global state management - Use dependency injection for services - Manage component-level state - Handle application-wide events - Implement undo/redo patterns 3. **Forms and Validation** - Build forms with EditForm - Implement data annotations validation - Handle custom validation - Display validation messages - Submit forms to API 4. **API Integration** - Call backend APIs from Blazor - Handle authentication tokens - Display loading states - Handle errors gracefully - Implement optimistic UI updates ## Load Additional Patterns - `.claude/patterns/api-patterns.md` ## Critical Rules ### Blazor Best Practices - Use `@rendermode` appropriately (Server, WebAssembly, Auto) - Dispose of resources in components (IDisposable) - Avoid blocking the UI thread - Use `StateHasChanged()` sparingly - Minimize JavaScript interop - Use cascading parameters for shared data - Implement proper error boundaries ### Component Design - Keep components focused (single responsibility) - Use parameters for component inputs - Use EventCallback for component outputs - Make components reusable - Separate presentation from logic - Use code-behind for complex logic ### Performance - Use `@key` directive for list items - Virtualize long lists - Lazy load routes and components - Minimize re-renders - Use OnInitializedAsync for async initialization - Stream large datasets ## Component Patterns ### Basic Component ```razor @* File: Components/BudgetCard.razor *@ @namespace {ApplicationName}.UI.Components

@Budget.Name

@Budget.Amount.ToString("C")

Created: @Budget.CreatedDate.ToString("d")

@code { [Parameter, EditorRequired] public BudgetResponse Budget { get; set; } = default!; [Parameter] public EventCallback OnEdit { get; set; } [Parameter] public EventCallback OnDelete { get; set; } private async Task OnEditClicked() { if (OnEdit.HasDelegate) await OnEdit.InvokeAsync(Budget); } private async Task OnDeleteClicked() { if (OnDelete.HasDelegate) await OnDelete.InvokeAsync(Budget.BudgetId); } } ``` ### Component with Code-Behind ```razor @* File: Pages/Budgets/BudgetList.razor *@ @page "/budgets" @namespace {ApplicationName}.UI.Pages.Budgets @inherits BudgetListBase Budgets

Budgets

@if (IsLoading) {

Loading budgets...

} else if (ErrorMessage is not null) {
@ErrorMessage
} else if (!Budgets.Any()) {

No budgets found. Create your first budget!

} else {
@foreach (var budget in Budgets) { }
}
``` ```csharp // File: Pages/Budgets/BudgetList.razor.cs using Microsoft.AspNetCore.Components; using {ApplicationName}.UI.Services; namespace {ApplicationName}.UI.Pages.Budgets; public class BudgetListBase : ComponentBase, IDisposable { [Inject] protected IBudgetService BudgetService { get; set; } = default!; [Inject] protected NavigationManager Navigation { get; set; } = default!; [Inject] protected ILogger Logger { get; set; } = default!; protected List Budgets { get; set; } = new(); protected bool IsLoading { get; set; } protected string? ErrorMessage { get; set; } protected override async Task OnInitializedAsync() { await LoadBudgetsAsync(); } protected async Task LoadBudgetsAsync() { IsLoading = true; ErrorMessage = null; try { Budgets = await BudgetService.GetBudgetsAsync(); } catch (Exception ex) { Logger.LogError(ex, "Error loading budgets"); ErrorMessage = "Failed to load budgets. Please try again."; } finally { IsLoading = false; } } protected void HandleCreateBudget() { Navigation.NavigateTo("/budgets/create"); } protected void HandleEditBudget(BudgetResponse budget) { Navigation.NavigateTo($"/budgets/{budget.BudgetId}/edit"); } protected async Task HandleDeleteBudget(Guid budgetId) { try { await BudgetService.DeleteBudgetAsync(budgetId); await LoadBudgetsAsync(); // Reload list } catch (Exception ex) { Logger.LogError(ex, "Error deleting budget {BudgetId}", budgetId); ErrorMessage = "Failed to delete budget. Please try again."; } } public void Dispose() { // Clean up subscriptions, timers, etc. } } ``` ## Form Validation Pattern ```razor @* File: Pages/Budgets/CreateBudget.razor *@ @page "/budgets/create" @namespace {ApplicationName}.UI.Pages.Budgets Create Budget

Create Budget

@if (ErrorMessage is not null) {
@ErrorMessage
}
@code { [Inject] private IBudgetService BudgetService { get; set; } = default!; [Inject] private NavigationManager Navigation { get; set; } = default!; [SupplyParameterFromForm] private CreateBudgetModel Model { get; set; } = new(); private bool IsSubmitting { get; set; } private string? ErrorMessage { get; set; } private async Task HandleValidSubmit() { IsSubmitting = true; ErrorMessage = null; try { var command = new CreateBudgetCommand( Model.Name, Model.Amount, Model.StartDate); var result = await BudgetService.CreateBudgetAsync(command); Navigation.NavigateTo($"/budgets/{result.BudgetId}"); } catch (Exception ex) { ErrorMessage = "Failed to create budget. Please try again."; } finally { IsSubmitting = false; } } private void Cancel() { Navigation.NavigateTo("/budgets"); } } ``` ```csharp // File: Models/CreateBudgetModel.cs using System.ComponentModel.DataAnnotations; public class CreateBudgetModel { [Required] [StringLength(100, MinimumLength = 3)] public string Name { get; set; } = string.Empty; [Required] [Range(0.01, 1_000_000)] public decimal Amount { get; set; } [Required] public DateTimeOffset StartDate { get; set; } = DateTimeOffset.Now; } ``` ## State Management (Fluxor) ### Install Fluxor ```bash dotnet add package Fluxor.Blazor.Web ``` ### Define State ```csharp // File: Store/BudgetState/BudgetState.cs namespace {ApplicationName}.UI.Store.BudgetState; public record BudgetState { public List Budgets { get; init; } = new(); public bool IsLoading { get; init; } public string? ErrorMessage { get; init; } } ``` ### Define Actions ```csharp // File: Store/BudgetState/BudgetActions.cs namespace {ApplicationName}.UI.Store.BudgetState; public record LoadBudgetsAction; public record LoadBudgetsSuccessAction(List Budgets); public record LoadBudgetsFailureAction(string ErrorMessage); ``` ### Implement Reducer ```csharp // File: Store/BudgetState/BudgetReducers.cs using Fluxor; namespace {ApplicationName}.UI.Store.BudgetState; public static class BudgetReducers { [ReducerMethod] public static BudgetState ReduceLoadBudgetsAction(BudgetState state, LoadBudgetsAction action) => state with { IsLoading = true, ErrorMessage = null }; [ReducerMethod] public static BudgetState ReduceLoadBudgetsSuccessAction(BudgetState state, LoadBudgetsSuccessAction action) => state with { IsLoading = false, Budgets = action.Budgets }; [ReducerMethod] public static BudgetState ReduceLoadBudgetsFailureAction(BudgetState state, LoadBudgetsFailureAction action) => state with { IsLoading = false, ErrorMessage = action.ErrorMessage }; } ``` ### Implement Effects ```csharp // File: Store/BudgetState/BudgetEffects.cs using Fluxor; namespace {ApplicationName}.UI.Store.BudgetState; public class BudgetEffects(IBudgetService budgetService) { [EffectMethod] public async Task HandleLoadBudgetsAction(LoadBudgetsAction action, IDispatcher dispatcher) { try { var budgets = await budgetService.GetBudgetsAsync(); dispatcher.Dispatch(new LoadBudgetsSuccessAction(budgets)); } catch (Exception ex) { dispatcher.Dispatch(new LoadBudgetsFailureAction(ex.Message)); } } } ``` ### Register Fluxor ```csharp // Program.cs builder.Services.AddFluxor(options => { options.ScanAssemblies(typeof(Program).Assembly); options.UseReduxDevTools(); }); ``` ### Use in Component ```razor @inherits FluxorComponent @inject IState BudgetState @inject IDispatcher Dispatcher
@if (BudgetState.Value.IsLoading) {

Loading...

} else { @foreach (var budget in BudgetState.Value.Budgets) { } }
@code { protected override void OnInitialized() { base.OnInitialized(); Dispatcher.Dispatch(new LoadBudgetsAction()); } } ``` ## API Service Pattern ```csharp // File: Services/BudgetService.cs namespace {ApplicationName}.UI.Services; using System.Net.Http.Json; public interface IBudgetService { Task> GetBudgetsAsync(CancellationToken cancellationToken = default); Task GetBudgetByIdAsync(Guid id, CancellationToken cancellationToken = default); Task CreateBudgetAsync(CreateBudgetCommand command, CancellationToken cancellationToken = default); Task UpdateBudgetAsync(Guid id, UpdateBudgetCommand command, CancellationToken cancellationToken = default); Task DeleteBudgetAsync(Guid id, CancellationToken cancellationToken = default); } public class BudgetService( HttpClient httpClient, ILogger logger ) : IBudgetService { public async Task> GetBudgetsAsync( CancellationToken cancellationToken = default) { try { var budgets = await httpClient.GetFromJsonAsync>( "/budgets", cancellationToken); return budgets ?? new List(); } catch (Exception ex) { logger.LogError(ex, "Error fetching budgets"); throw; } } public async Task GetBudgetByIdAsync( Guid id, CancellationToken cancellationToken = default) { try { var budget = await httpClient.GetFromJsonAsync( $"/budgets/{id}", cancellationToken); return budget ?? throw new InvalidOperationException("Budget not found"); } catch (Exception ex) { logger.LogError(ex, "Error fetching budget {BudgetId}", id); throw; } } public async Task CreateBudgetAsync( CreateBudgetCommand command, CancellationToken cancellationToken = default) { try { var response = await httpClient.PostAsJsonAsync( "/budgets", command, cancellationToken); response.EnsureSuccessStatusCode(); var result = await response.Content.ReadFromJsonAsync( cancellationToken: cancellationToken); return result ?? throw new InvalidOperationException("Failed to create budget"); } catch (Exception ex) { logger.LogError(ex, "Error creating budget"); throw; } } public async Task UpdateBudgetAsync( Guid id, UpdateBudgetCommand command, CancellationToken cancellationToken = default) { try { var response = await httpClient.PutAsJsonAsync( $"/budgets/{id}", command, cancellationToken); response.EnsureSuccessStatusCode(); } catch (Exception ex) { logger.LogError(ex, "Error updating budget {BudgetId}", id); throw; } } public async Task DeleteBudgetAsync( Guid id, CancellationToken cancellationToken = default) { try { var response = await httpClient.DeleteAsync( $"/budgets/{id}", cancellationToken); response.EnsureSuccessStatusCode(); } catch (Exception ex) { logger.LogError(ex, "Error deleting budget {BudgetId}", id); throw; } } } // Register service builder.Services.AddScoped(); ``` ## JavaScript Interop ```razor @inject IJSRuntime JS @code { private async Task ShowAlert() { await JS.InvokeVoidAsync("alert", "Hello from Blazor!"); } private async Task GetLocalStorage() { var value = await JS.InvokeAsync("localStorage.getItem", "myKey"); Console.WriteLine($"Value from localStorage: {value}"); } private async Task SetLocalStorage() { await JS.InvokeVoidAsync("localStorage.setItem", "myKey", "myValue"); } } ``` ```javascript // wwwroot/js/interop.js window.budgetApp = { formatCurrency: function (amount) { return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(amount); }, showConfirmDialog: function (message) { return confirm(message); }, downloadFile: function (filename, content) { const blob = new Blob([content], { type: 'text/plain' }); const url = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; a.click(); window.URL.revokeObjectURL(url); } }; ``` ## Authentication in Blazor ```razor @* File: Pages/Login.razor *@ @page "/login" @inject NavigationManager Navigation @inject IAuthenticationService AuthService @code { private LoginModel loginModel = new(); private async Task HandleLogin() { var result = await AuthService.LoginAsync(loginModel.Email, loginModel.Password); if (result.Success) { Navigation.NavigateTo("/"); } } } ``` ```razor @* File: Components/AuthorizeView.razor *@ @using Microsoft.AspNetCore.Components.Authorization

Hello, @context.User.Identity?.Name!

Logout
Login
``` ## Performance Optimization ### Virtualization ```razor @using Microsoft.AspNetCore.Components.Web.Virtualization ``` ### Lazy Loading ```razor @* File: App.razor *@ Not found

Sorry, there's nothing at this address.

``` ## Common Blazor Pitfalls ### ❌ Avoid These Mistakes 1. **Not Disposing Components** - ❌ Subscribe to events without unsubscribing - ✅ Implement IDisposable and clean up 2. **Blocking UI Thread** - ❌ Using `Task.Result` or `.Wait()` - ✅ Always use `await` 3. **Overusing StateHasChanged** - ❌ Calling `StateHasChanged()` everywhere - ✅ Let Blazor handle rendering automatically 4. **Missing @key Directive** - ❌ Rendering lists without `@key` - ✅ Use `@key` for dynamic lists 5. **Not Handling Errors** - ❌ No error boundaries - ✅ Use ErrorBoundary component ## Blazor Checklist ### Component Development - [ ] Components are focused and reusable - [ ] Parameters use `[Parameter]` attribute - [ ] EventCallbacks for component events - [ ] Code-behind for complex logic - [ ] IDisposable implemented where needed ### Forms & Validation - [ ] EditForm used for forms - [ ] Data annotations validation - [ ] ValidationSummary displayed - [ ] Submit button disabled during submission - [ ] Error messages displayed ### State Management - [ ] Global state managed (Fluxor/FluxState) - [ ] Component state localized - [ ] Services injected via DI - [ ] State changes trigger re-renders ### API Integration - [ ] HttpClient configured - [ ] Loading states displayed - [ ] Error handling implemented - [ ] Authentication tokens included - [ ] Retry logic for transient failures ### Performance - [ ] Virtualization for long lists - [ ] Lazy loading for routes - [ ] @key directive on lists - [ ] Minimal JavaScript interop - [ ] Component disposal implemented ## Checklist Before Completion - [ ] All components render correctly - [ ] Forms validate and submit - [ ] API calls successful - [ ] Loading states displayed - [ ] Error handling functional - [ ] Authentication working - [ ] State management functional - [ ] Performance optimized - [ ] Responsive design implemented - [ ] Documentation complete