Skip to main content
Technology & EngineeringCsharp Dotnet299 lines

Blazor

Blazor component model, rendering modes, and interactive web UI development in .NET

Quick Summary23 lines
You are an expert in Blazor for building interactive web UIs with C# instead of JavaScript.

## Key Points

- **Use `[EditorRequired]` on mandatory parameters** so the compiler flags missing values at build time.
- **Implement `IDisposable`** on components that subscribe to events or create timers to prevent memory leaks.
- **Use `@key` on list items** to help Blazor's diffing algorithm preserve DOM state correctly.
- **Minimize JS interop** — only call JavaScript for browser APIs that have no Blazor equivalent (e.g., clipboard, geolocation).
- **Calling `StateHasChanged` from a non-UI thread** — in Interactive Server mode, use `InvokeAsync(StateHasChanged)` when updating state from a background task or event.
- **Large WASM download size** — the initial .NET runtime download for WebAssembly can be several MB. Use trimming, AOT compilation, and lazy-loaded assemblies to reduce payload.
- **Forgetting `[SupplyParameterFromForm]` in SSR forms** — without it, the form model is always default after postback in static SSR mode.
- **Using `NavigationManager` before `OnAfterRender`** — navigation calls during `OnInitialized` can fail in prerender scenarios. Guard with `NavigationManager.NavigateTo()` only after render.

## Quick Example

```csharp
@foreach (var item in items)
{
    <ItemRow @key="item.Id" Item="item" />
}
```
skilldb get csharp-dotnet-skills/BlazorFull skill: 299 lines
Paste into your CLAUDE.md or agent config

Blazor — C#/.NET

You are an expert in Blazor for building interactive web UIs with C# instead of JavaScript.

Core Philosophy

Overview

Blazor is a .NET web UI framework that lets developers build interactive web applications using C# and Razor syntax. As of .NET 8, Blazor supports multiple rendering modes: Static Server-Side Rendering (SSR), Interactive Server (SignalR), Interactive WebAssembly (WASM), and Auto mode that starts with Server and transitions to WASM. Components are the core building block.

Core Concepts

Component Structure

@page "/counter"
@rendermode InteractiveServer

<PageTitle>Counter</PageTitle>

<h1>Counter</h1>
<p role="status">Current count: @currentCount</p>
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

@code {
    private int currentCount = 0;

    [Parameter]
    public int InitialCount { get; set; } = 0;

    protected override void OnInitialized()
    {
        currentCount = InitialCount;
    }

    private void IncrementCount()
    {
        currentCount++;
    }
}

Rendering Modes (.NET 8+)

// In App.razor or individual components
@rendermode InteractiveServer      // SignalR connection, runs on server
@rendermode InteractiveWebAssembly // Runs in browser via WASM
@rendermode InteractiveAuto        // Server first, then WASM after download

// Static SSR (default, no @rendermode attribute)
// Renders HTML on server, no interactivity

Parameters and Cascading Values

// Child component: ProductCard.razor
<div class="card">
    <h3>@Product.Name</h3>
    <p>@Product.Price.ToString("C")</p>
    <button @onclick="() => OnAddToCart.InvokeAsync(Product)">Add to Cart</button>
</div>

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

    [Parameter]
    public EventCallback<ProductDto> OnAddToCart { get; set; }

    [CascadingParameter]
    public Theme CurrentTheme { get; set; } = default!;
}

// Parent usage
<CascadingValue Value="theme">
    @foreach (var product in products)
    {
        <ProductCard Product="product" OnAddToCart="HandleAddToCart" />
    }
</CascadingValue>

Lifecycle Methods

@code {
    // Called when parameters are set or changed
    protected override void OnParametersSet() { }

    // Called once after first render
    protected override async Task OnInitializedAsync()
    {
        products = await ProductService.GetAllAsync();
    }

    // Called after every render
    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            // Safe to use JS interop here
            await JS.InvokeVoidAsync("initializeChart", chartElement);
        }
    }

    public void Dispose()
    {
        // Clean up event handlers, timers, etc.
    }
}

Implementation Patterns

Forms and Validation

@page "/orders/new"
@rendermode InteractiveServer
@inject IOrderService OrderService
@inject NavigationManager Nav

<EditForm Model="model" OnValidSubmit="HandleSubmit" FormName="create-order">
    <DataAnnotationsValidator />
    <ValidationSummary />

    <div class="mb-3">
        <label for="product">Product Name</label>
        <InputText id="product" @bind-Value="model.ProductName" class="form-control" />
        <ValidationMessage For="() => model.ProductName" />
    </div>

    <div class="mb-3">
        <label for="qty">Quantity</label>
        <InputNumber id="qty" @bind-Value="model.Quantity" class="form-control" />
    </div>

    <button type="submit" class="btn btn-primary" disabled="@isSubmitting">
        @(isSubmitting ? "Submitting..." : "Place Order")
    </button>
</EditForm>

@code {
    [SupplyParameterFromForm]
    private CreateOrderModel model { get; set; } = new();
    private bool isSubmitting;

    private async Task HandleSubmit()
    {
        isSubmitting = true;
        await OrderService.CreateAsync(model);
        Nav.NavigateTo("/orders");
    }
}

public class CreateOrderModel
{
    [Required, StringLength(100)]
    public string ProductName { get; set; } = "";

    [Range(1, 1000)]
    public int Quantity { get; set; } = 1;
}

Component Communication via State Service

public class CartState
{
    private readonly List<CartItem> _items = [];

    public IReadOnlyList<CartItem> Items => _items;
    public decimal Total => _items.Sum(i => i.Price * i.Quantity);

    public event Action? OnChange;

    public void AddItem(CartItem item)
    {
        _items.Add(item);
        OnChange?.Invoke();
    }

    public void Clear()
    {
        _items.Clear();
        OnChange?.Invoke();
    }
}

// Register as scoped (per-circuit in Server mode)
builder.Services.AddScoped<CartState>();

// In a component
@inject CartState Cart
@implements IDisposable

<span class="badge">@Cart.Items.Count items — @Cart.Total.ToString("C")</span>

@code {
    protected override void OnInitialized() => Cart.OnChange += StateHasChanged;
    public void Dispose() => Cart.OnChange -= StateHasChanged;
}

JS Interop

@inject IJSRuntime JS

<div @ref="mapElement" style="height: 400px;"></div>

@code {
    private ElementReference mapElement;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            await JS.InvokeVoidAsync("initMap", mapElement, 51.505, -0.09);
        }
    }
}

Streaming Rendering

@page "/reports"
@attribute [StreamRendering]

@if (reports is null)
{
    <p>Loading reports...</p>
}
else
{
    <table>
        @foreach (var report in reports)
        {
            <tr><td>@report.Name</td><td>@report.Total</td></tr>
        }
    </table>
}

@code {
    private List<ReportDto>? reports;

    protected override async Task OnInitializedAsync()
    {
        // Initial HTML ships immediately with "Loading..." placeholder.
        // When the await completes, Blazor streams the updated HTML.
        reports = await ReportService.GetAllAsync();
    }
}

Best Practices

  • Choose the right render mode — use Static SSR for content pages, Interactive Server for internal apps with low latency needs, WASM for offline-capable or CDN-served apps, and Auto for the best of both.
  • Use [EditorRequired] on mandatory parameters so the compiler flags missing values at build time.
  • Implement IDisposable on components that subscribe to events or create timers to prevent memory leaks.
  • Use @key on list items to help Blazor's diffing algorithm preserve DOM state correctly.
@foreach (var item in items)
{
    <ItemRow @key="item.Id" Item="item" />
}
  • Minimize JS interop — only call JavaScript for browser APIs that have no Blazor equivalent (e.g., clipboard, geolocation).

Common Pitfalls

  • Calling StateHasChanged from a non-UI thread — in Interactive Server mode, use InvokeAsync(StateHasChanged) when updating state from a background task or event.
  • Large WASM download size — the initial .NET runtime download for WebAssembly can be several MB. Use trimming, AOT compilation, and lazy-loaded assemblies to reduce payload.
  • Forgetting [SupplyParameterFromForm] in SSR forms — without it, the form model is always default after postback in static SSR mode.
  • Using NavigationManager before OnAfterRender — navigation calls during OnInitialized can fail in prerender scenarios. Guard with NavigationManager.NavigateTo() only after render.
  • Overusing StateHasChanged — Blazor automatically re-renders after event handlers complete. Calling it explicitly is only needed when state changes outside of Blazor events (e.g., from a timer or external subscription).

Anti-Patterns

Over-engineering for hypothetical scale. Building for millions of users when you have hundreds adds complexity without value. Solve today's problems first.

Ignoring the existing ecosystem. Reinventing functionality that mature libraries already provide well wastes time and introduces unnecessary risk.

Premature abstraction. Creating elaborate frameworks and utilities before you have enough concrete cases to know what the abstraction should look like produces the wrong abstraction.

Neglecting error handling at boundaries. Internal code can trust its inputs, but system boundaries (user input, APIs, file I/O) require defensive validation.

Skipping documentation for obvious code. What is obvious to you today will not be obvious to your colleague next month or to you next year.

Install this skill directly: skilldb add csharp-dotnet-skills

Get CLI access →