# Blazor Specialist

> Blazor UI development specialist. Use for building Blazor Server or WebAssembly apps, component development, state management, or form handling. Use when this capability is needed.

- Skill: `tomevault-io/blazor-specialist` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add tomevault-io/blazor-specialist`
- Raw SKILL.md: https://api.skillmd.com/api/skills/tomevault-io/blazor-specialist/raw
- Safety review: pending (external: skill-scanner PASS, skillspector PASS)
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- Author: tomevault-io (https://skillmd.com/u/tomevault-io)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/tomevault-io/blazor-specialist

---


# 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

<div class="budget-card">
    <h3>@Budget.Name</h3>
    <p class="amount">@Budget.Amount.ToString("C")</p>
    <p class="created">Created: @Budget.CreatedDate.ToString("d")</p>

    <button @onclick="OnEditClicked">Edit</button>
    <button @onclick="OnDeleteClicked" class="danger">Delete</button>
</div>

@code {
    [Parameter, EditorRequired]
    public BudgetResponse Budget { get; set; } = default!;

    [Parameter]
    public EventCallback<BudgetResponse> OnEdit { get; set; }

    [Parameter]
    public EventCallback<Guid> 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

<PageTitle>Budgets</PageTitle>

<div class="budget-list-page">
    <h1>Budgets</h1>

    @if (IsLoading)
    {
        <p>Loading budgets...</p>
    }
    else if (ErrorMessage is not null)
    {
        <div class="error">@ErrorMessage</div>
    }
    else if (!Budgets.Any())
    {
        <p>No budgets found. Create your first budget!</p>
    }
    else
    {
        <div class="budget-grid">
            @foreach (var budget in Budgets)
            {
                <BudgetCard
                    Budget="@budget"
                    OnEdit="@HandleEditBudget"
                    OnDelete="@HandleDeleteBudget" />
            }
        </div>
    }

    <button @onclick="HandleCreateBudget" class="primary">Create Budget</button>
</div>
```

```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<BudgetListBase> Logger { get; set; } = default!;

    protected List<BudgetResponse> 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

<PageTitle>Create Budget</PageTitle>

<div class="create-budget-page">
    <h1>Create Budget</h1>

    <EditForm Model="@Model" OnValidSubmit="@HandleValidSubmit" FormName="CreateBudgetForm">
        <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="amount">Amount:</label>
            <InputNumber id="amount" @bind-Value="Model.Amount" class="form-control" />
            <ValidationMessage For="@(() => Model.Amount)" />
        </div>

        <div class="form-group">
            <label for="startDate">Start Date:</label>
            <InputDate id="startDate" @bind-Value="Model.StartDate" class="form-control" />
            <ValidationMessage For="@(() => Model.StartDate)" />
        </div>

        <div class="form-actions">
            <button type="submit" class="btn btn-primary" disabled="@IsSubmitting">
                @if (IsSubmitting)
                {
                    <span>Creating...</span>
                }
                else
                {
                    <span>Create</span>
                }
            </button>
            <button type="button" class="btn btn-secondary" @onclick="@Cancel">Cancel</button>
        </div>

        @if (ErrorMessage is not null)
        {
            <div class="alert alert-danger mt-3">@ErrorMessage</div>
        }
    </EditForm>
</div>

@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<BudgetResponse> 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<BudgetResponse> 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> BudgetState
@inject IDispatcher Dispatcher

<div>
    @if (BudgetState.Value.IsLoading)
    {
        <p>Loading...</p>
    }
    else
    {
        @foreach (var budget in BudgetState.Value.Budgets)
        {
            <BudgetCard Budget="@budget" />
        }
    }
</div>

@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<List<BudgetResponse>> GetBudgetsAsync(CancellationToken cancellationToken = default);
    Task<BudgetResponse> GetBudgetByIdAsync(Guid id, CancellationToken cancellationToken = default);
    Task<CreateBudgetResponse> 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<BudgetService> logger
) : IBudgetService
{
    public async Task<List<BudgetResponse>> GetBudgetsAsync(
        CancellationToken cancellationToken = default)
    {
        try
        {
            var budgets = await httpClient.GetFromJsonAsync<List<BudgetResponse>>(
                "/budgets",
                cancellationToken);

            return budgets ?? new List<BudgetResponse>();
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "Error fetching budgets");
            throw;
        }
    }

    public async Task<BudgetResponse> GetBudgetByIdAsync(
        Guid id,
        CancellationToken cancellationToken = default)
    {
        try
        {
            var budget = await httpClient.GetFromJsonAsync<BudgetResponse>(
                $"/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<CreateBudgetResponse> CreateBudgetAsync(
        CreateBudgetCommand command,
        CancellationToken cancellationToken = default)
    {
        try
        {
            var response = await httpClient.PostAsJsonAsync(
                "/budgets",
                command,
                cancellationToken);

            response.EnsureSuccessStatusCode();

            var result = await response.Content.ReadFromJsonAsync<CreateBudgetResponse>(
                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<IBudgetService, BudgetService>();
```

## JavaScript Interop

```razor
@inject IJSRuntime JS

<button @onclick="ShowAlert">Show Alert</button>
<button @onclick="GetLocalStorage">Get from LocalStorage</button>

@code {
    private async Task ShowAlert()
    {
        await JS.InvokeVoidAsync("alert", "Hello from Blazor!");
    }

    private async Task GetLocalStorage()
    {
        var value = await JS.InvokeAsync<string>("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

<div class="login-page">
    <h1>Login</h1>

    <EditForm Model="@loginModel" OnValidSubmit="@HandleLogin">
        <DataAnnotationsValidator />
        <ValidationSummary />

        <div class="form-group">
            <label>Email:</label>
            <InputText @bind-Value="loginModel.Email" class="form-control" />
        </div>

        <div class="form-group">
            <label>Password:</label>
            <InputText type="password" @bind-Value="loginModel.Password" class="form-control" />
        </div>

        <button type="submit" class="btn btn-primary">Login</button>
    </EditForm>
</div>

@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

<AuthorizeView>
    <Authorized>
        <p>Hello, @context.User.Identity?.Name!</p>
        <a href="/logout">Logout</a>
    </Authorized>
    <NotAuthorized>
        <a href="/login">Login</a>
    </NotAuthorized>
</AuthorizeView>
```

## Performance Optimization

### Virtualization
```razor
@using Microsoft.AspNetCore.Components.Web.Virtualization

<Virtualize Items="@budgets" Context="budget">
    <BudgetCard Budget="@budget" />
</Virtualize>
```

### Lazy Loading
```razor
@* File: App.razor *@
<Router AppAssembly="@typeof(Program).Assembly">
    <Found Context="routeData">
        <RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
    </Found>
    <NotFound>
        <PageTitle>Not found</PageTitle>
        <p>Sorry, there's nothing at this address.</p>
    </NotFound>
</Router>
```

## 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

---
> Converted and distributed by [TomeVault](https://tomevault.io/claim/i-synergy) — claim your Tome and manage your conversions.
<!-- tomevault:4.0:skill_md:2026-04-14 -->

