Claude Skill

dotnet-blazor

Build and review Blazor applications across server, WebAssembly, web app, and hybrid scenarios with correct component design, state flow, rendering, and hosting choices.

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download postpartum-genushyacinthus29-dotnet-skills-skills_dotnet-blazor-bfa4ebd.zip · 11 KB
Part of postpartum-genushyacinthus29/dotnet-skills — 80 skills

Install

skills CLI npx skills add https://github.com/Postpartum-genushyacinthus29/dotnet-skills/tree/main/skills/dotnet-blazor
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install postpartum-genushyacinthus29-dotnet-skills@llmmart
Git git clone https://github.com/Postpartum-genushyacinthus29/dotnet-skills.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole postpartum-genushyacinthus29/dotnet-skills collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

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 needed

Workflow

  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)

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
Files (dotnet-skills)
  • references
    • anti-patterns.md 13.5 KB
      # Blazor Anti-Patterns
      
      ## Component Design Anti-Patterns
      
      ### Monolithic Components
      
      **Problem:** Creating large components that handle multiple concerns.
      
      ```razor
      @* 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.
      
      ```razor
      @* GOOD: Composed from smaller components *@
      <ProductPage>
          <ProductFilters />
          <ProductGrid Products="@products" />
          <CartSidebar />
      </ProductPage>
      ```
      
      ### Parameter Drilling
      
      **Problem:** Passing parameters through many component layers.
      
      ```razor
      @* 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.
      
      ```razor
      @* 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.
      
      ```razor
      @* 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.
      
      ```razor
      @* 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.
      
      ```razor
      @* BAD: No indication this is required *@
      @code {
          [Parameter] public Product Product { get; set; } = default!;
      }
      ```
      
      **Solution:** Use EditorRequired for mandatory parameters.
      
      ```razor
      @* 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.
      
      ```csharp
      // 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.
      
      ```csharp
      // 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.
      
      ```razor
      @* BAD: Never unsubscribes *@
      @code {
          protected override void OnInitialized()
          {
              CartService.OnChange += StateHasChanged;
          }
      }
      ```
      
      **Solution:** Implement IDisposable.
      
      ```razor
      @* 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).
      
      ```razor
      @* 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.
      
      ```razor
      @* 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.
      
      ```csharp
      // 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.
      
      ```csharp
      // 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 HttpClient
      ```
      
      ### Ignoring Render Mode Boundaries
      
      **Problem:** Assuming all components run in the same mode.
      
      ```razor
      @* BAD: Child assumes parent's render mode *@
      <InteractiveParent>
          <StaticChild />  @* May not behave as expected *@
      </InteractiveParent>
      ```
      
      **Solution:** Explicitly set render modes and understand boundaries.
      
      ```razor
      @* 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.
      
      ```csharp
      // BAD: Only works on server
      services.AddScoped<IDataService, ServerOnlyDataService>();
      ```
      
      **Solution:** Register environment-specific implementations.
      
      ```csharp
      // 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.
      
      ```razor
      @* BAD: No key, poor diffing *@
      @foreach (var item in items)
      {
          <ItemComponent Item="@item" />
      }
      ```
      
      **Solution:** Use @key for efficient updates.
      
      ```razor
      @* 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.
      
      ```razor
      @* BAD: Renders all 10,000 items *@
      @foreach (var item in allItems)
      {
          <ItemRow Item="@item" />
      }
      ```
      
      **Solution:** Use Virtualize component.
      
      ```razor
      @* 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.
      
      ```razor
      @* BAD: Re-renders on every parent change *@
      <ExpensiveComponent Data="@unchangingData" />
      ```
      
      **Solution:** Override ShouldRender for expensive components.
      
      ```razor
      @* 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.
      
      ```csharp
      // BAD: Blocks the thread
      protected override void OnInitialized()
      {
          var data = Http.GetFromJsonAsync<Data>("api/data").Result; // BLOCKS!
      }
      ```
      
      **Solution:** Use proper async patterns.
      
      ```csharp
      // 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.
      
      ```csharp
      // BAD: Blocks in Server mode
      var result = ((IJSInProcessRuntime)JS).Invoke<string>("getValue");
      ```
      
      **Solution:** Always use async JS interop.
      
      ```csharp
      // GOOD: Non-blocking
      var result = await JS.InvokeAsync<string>("getValue");
      ```
      
      ### JS Interop During Prerendering
      
      **Problem:** Calling JS during prerender when there's no browser.
      
      ```csharp
      // 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.
      
      ```csharp
      // 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.
      
      ```csharp
      // 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.
      
      ```csharp
      // 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.
      
      ```csharp
      // BAD: Serializes entire dataset
      await JS.InvokeVoidAsync("processData", hugeDataSet);
      ```
      
      **Solution:** Use streaming or pass references.
      
      ```csharp
      // 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.
      
      ```razor
      @* BAD: No validation *@
      <EditForm Model="@model" OnSubmit="Submit">
          <InputText @bind-Value="model.Email" />
          <button type="submit">Submit</button>
      </EditForm>
      ```
      
      **Solution:** Add validators.
      
      ```razor
      @* 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.
      
      ```razor
      @* BAD: Can submit multiple times *@
      <button type="submit">Submit</button>
      ```
      
      **Solution:** Track and display submission state.
      
      ```razor
      @* 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.
      
      ```razor
      @* BAD: Unhandled exception crashes circuit *@
      <RiskyComponent />
      ```
      
      **Solution:** Wrap risky components in ErrorBoundary.
      
      ```razor
      @* 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.
      
      ```csharp
      // BAD: Silent failure
      try
      {
          await SaveDataAsync();
      }
      catch
      {
          // Swallowed - user has no idea it failed
      }
      ```
      
      **Solution:** Provide feedback and logging.
      
      ```csharp
      // 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.
      
      ```razor
      @* BAD: Client-side only - easily bypassed *@
      @if (isAdmin)
      {
          <AdminPanel />
      }
      ```
      
      **Solution:** Always validate on server.
      
      ```csharp
      // 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.
      
      ```razor
      @* BAD: API key visible in browser state *@
      @code {
          private string apiKey = "secret-api-key-12345";
      }
      ```
      
      **Solution:** Keep secrets server-side only.
      
      ```csharp
      // 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.
      
      ```csharp
      // BAD: Direct use of user input
      var userId = userIdFromClient;
      var data = await Db.GetUserData(userId); // Can access any user's data
      ```
      
      **Solution:** Validate against authenticated user.
      
      ```csharp
      // GOOD: Validate ownership
      var authenticatedUserId = GetAuthenticatedUserId();
      if (requestedUserId != authenticatedUserId)
      {
          throw new UnauthorizedAccessException();
      }
      ```
      
    • patterns.md 15.6 KB
      # 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)**
      ```razor
      @* 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)**
      ```razor
      @* 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.
      
      ```razor
      @* 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:**
      ```razor
      <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.
      
      ```razor
      @* 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.
      
      ```razor
      @* 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;
      }
      ```
      
      ```razor
      @* 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.
      
      ```csharp
      // 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;
              }
          }
      }
      ```
      
      ```razor
      @* 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.
      
      ```razor
      @code {
          private int count = 0;
          private string message = "";
      
          private void Increment() => count++;
      }
      ```
      
      ### Service-Based Shared State
      
      For state shared across multiple components using DI.
      
      ```csharp
      // 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();
      }
      ```
      
      ```razor
      @* 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.
      
      ```csharp
      // 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
          }
      }
      ```
      
      ```razor
      @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.
      
      ```razor
      @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
      
      ```razor
      @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.
      
      ```javascript
      // 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();
      }
      ```
      
      ```razor
      @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.
      
      ```csharp
      // 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
      
      ```razor
      @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
      
      ```csharp
      // Stream large data to JavaScript
      using var streamRef = new DotNetStreamReference(stream: myLargeDataStream);
      await JS.InvokeVoidAsync("receiveStream", streamRef);
      ```
      
      ```javascript
      async function receiveStream(streamRef) {
          const data = await streamRef.arrayBuffer();
          // Process data
      }
      ```
      
      ## Advanced Patterns
      
      ### Dynamic Component Loading
      
      ```razor
      <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.
      
      ```razor
      @* StaticLayout.razor - No render mode *@
      <header>
          <nav>Static navigation</nav>
      </header>
      
      <main>
          @Body
      </main>
      
      <footer>Static footer</footer>
      ```
      
      ```razor
      @* InteractiveDashboard.razor *@
      @rendermode InteractiveServer
      
      <div class="dashboard">
          <RealTimeChart />
          <LiveNotifications />
      </div>
      ```
      
      ### Error Boundary Pattern
      
      ```razor
      <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.
      
      ```razor
      @* MainLayout.razor *@
      <header>
          <SectionOutlet SectionName="PageHeader" />
      </header>
      
      <main>@Body</main>
      
      <aside>
          <SectionOutlet SectionName="Sidebar" />
      </aside>
      ```
      
      ```razor
      @* ProductPage.razor *@
      <SectionContent SectionName="PageHeader">
          <h1>Products</h1>
          <SearchBar />
      </SectionContent>
      
      <SectionContent SectionName="Sidebar">
          <CategoryFilter />
          <PriceRangeFilter />
      </SectionContent>
      
      <ProductGrid Products="@products" />
      ```
      
      ### Render Optimization with ShouldRender
      
      ```razor
      @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
      
      ```razor
      <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;
              }
          }
      }
      ```
      
  • SKILL.md 7 KB
    ---
    name: dotnet-blazor
    version: "1.0.0"
    category: "Web"
    description: "Build and review Blazor applications across server, WebAssembly, web app, and hybrid scenarios with correct component design, state flow, rendering, and hosting choices."
    compatibility: "Requires Blazor project (.NET 6+, preferably .NET 8+ for unified model)."
    ---
    
    # 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
    
    - [Blazor Overview](https://learn.microsoft.com/en-us/aspnet/core/blazor/?view=aspnetcore-10.0)
    - [Render Modes](https://learn.microsoft.com/en-us/aspnet/core/blazor/components/render-modes?view=aspnetcore-10.0)
    - [Performance Best Practices](https://learn.microsoft.com/en-us/aspnet/core/blazor/performance?view=aspnetcore-10.0)
    - [State Management](https://learn.microsoft.com/en-us/aspnet/core/blazor/state-management?view=aspnetcore-10.0)
    - [JS Interop](https://learn.microsoft.com/en-us/aspnet/core/blazor/javascript-interoperability/?view=aspnetcore-10.0)
    
    ### References
    
    - [patterns.md](references/patterns.md) - Detailed component patterns, state management strategies, and JS interop techniques
    - [anti-patterns.md](references/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
    
    ```razor
    @* 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 needed
    ```
    
    ## Workflow
    
    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)
    
    ## Component Patterns
    
    ### Basic Component
    ```razor
    @* 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
    ```razor
    @* 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+)
    ```razor
    @* 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
    
    ```csharp
    // 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:**
       ```razor
       <Virtualize Items="@products" Context="product">
           <ProductCard Product="@product" />
       </Virtualize>
       ```
    
    2. **Use `@key` for list diffing:**
       ```razor
       @foreach (var item in items)
       {
           <ItemComponent @key="item.Id" Item="@item" />
       }
       ```
    
    3. **Debounce rapid events:**
       ```csharp
       private Timer? _debounceTimer;
    
       private void OnInput(ChangeEventArgs e)
       {
           _debounceTimer?.Dispose();
           _debounceTimer = new Timer(_ => InvokeAsync(DoSearch), null, 300, Timeout.Infinite);
       }
       ```
    
    4. **Lazy load assemblies (WASM):**
       ```csharp
       var assemblies = await LazyAssemblyLoader
           .LoadAssembliesAsync(["MyHeavyFeature.wasm"]);
       ```
    
    ## JS Interop
    
    ### Calling JavaScript from C#
    ```csharp
    @inject IJSRuntime JS
    
    await JS.InvokeVoidAsync("alert", "Hello from Blazor!");
    var result = await JS.InvokeAsync<string>("prompt", "Enter name:");
    ```
    
    ### Calling C# from JavaScript
    ```csharp
    [JSInvokable]
    public static string GetMessage() => "Hello from C#!";
    ```
    
    ```javascript
    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
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related