Files
Fixiy/.github/skills/blazor-specialist/SKILL.md
T
2026-06-04 17:11:11 +02:00

21 KiB

name, description
name description
blazor-specialist 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

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

@* 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>
// 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

@* 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");
    }
}
// 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

dotnet add package Fluxor.Blazor.Web

Define State

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

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

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

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

// Program.cs
builder.Services.AddFluxor(options =>
{
    options.ScanAssemblies(typeof(Program).Assembly);
    options.UseReduxDevTools();
});

Use in Component

@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

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

@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");
    }
}
// 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

@* 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("/");
        }
    }
}
@* 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

@using Microsoft.AspNetCore.Components.Web.Virtualization

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

Lazy Loading

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