blazor
Blazor
Trigger On
- working on
.razorfiles,.razor.cscode-behind,_Imports.razor - choosing between Server, WebAssembly, Auto, hoặc SSR render mode
- component parameters,
EventCallback, cascading values, two-way binding@bind - component lifecycle (
OnInitializedAsync,OnAfterRenderAsync,ShouldRender) EditForm+ DataAnnotations / FluentValidation- JS interop (
IJSRuntime.InvokeAsync) - state management (component, service-based, Fluxor/Blazored)
- migrate Blazor Server-only / WASM-only sang unified Blazor Web App (.NET 8)
Documentation
Render Modes (.NET 8+ unified)
Đây là điểm quan trọng nhất khi làm Blazor hiện đại — pick đúng render mode theo context của TỪNG component, không pick globally.
| Mode | Code chạy ở | First render | Interactive | Use case |
|---|---|---|---|---|
| Static SSR (default) | Server | Server HTML | ❌ Không (chỉ HTML) | Marketing page, blog, content tĩnh |
| Stream Rendering | Server | Server (streaming) | ❌ | Page có data slow load — render skeleton + stream khi data ready |
| InteractiveServer | Server | Server | ✓ qua SignalR | Admin dashboard, internal tool, complex form |
| InteractiveWebAssembly | Browser | Server (prerender) → Browser | ✓ client-side | App offline-capable, low server load |
| InteractiveAuto | Server (initial) → WASM | Server | ✓ chuyển dần | Best UX nhưng require WASM download lần đầu |
@* Per-component render mode *@
@page "/dashboard"
@rendermode InteractiveServer
<MyDashboard />
@* Per-component khi gọi *@
<Counter @rendermode="InteractiveWebAssembly" />
Quyết nhanh
- App marketing/blog →
Static SSR(default), không thêm gì - Admin/internal có form, table interactive →
InteractiveServer(rẻ, latency phụ thuộc network) - App khách hàng cần offline / scale lớn →
InteractiveWebAssembly(download initial) - Dùng feature React-like (LocalStorage, IndexedDB) → WASM
- Không chắc →
InteractiveAutocho UX tốt nhất
Component Patterns
Parameters
@* Counter.razor *@
<button @onclick="IncrementCount">@Title: @currentCount</button>
@code {
[Parameter] public string Title { get; set; } = "Count";
[Parameter] public int Initial { get; set; } = 0;
[Parameter] public EventCallback<int> OnChanged { get; set; }
[Parameter, EditorRequired] public string Required { get; set; } = "";
private int currentCount;
protected override void OnInitialized() => currentCount = Initial;
private async Task IncrementCount()
{
currentCount++;
await OnChanged.InvokeAsync(currentCount); // notify parent
}
}
Two-way binding @bind
@* Parent *@
<MyInput @bind-Value="userName" @bind-Value:after="OnNameChanged" />
@* MyInput.razor *@
<input value="@Value" @oninput="HandleInput" />
@code {
[Parameter] public string Value { get; set; } = "";
[Parameter] public EventCallback<string> ValueChanged { get; set; }
private async Task HandleInput(ChangeEventArgs e)
{
var newValue = e.Value?.ToString() ?? "";
await ValueChanged.InvokeAsync(newValue);
}
}
Cascading Values — share data qua component tree
@* Parent *@
<CascadingValue Value="@currentTheme" Name="Theme">
<Layout>
@Body
</Layout>
</CascadingValue>
@* Child sâu trong tree *@
@code {
[CascadingParameter(Name = "Theme")]
public Theme? Theme { get; set; }
}
Dùng cho dữ liệu shared toàn UI (theme, current user, culture). KHÔNG abuse — dữ liệu nghiệp vụ → service DI.
Code-behind pattern
Component lớn → tách .razor (markup) khỏi .razor.cs (logic):
@* Counter.razor *@
@inherits CounterBase
<button @onclick="Increment">Count: @Count</button>
// Counter.razor.cs
public partial class CounterBase : ComponentBase
{
[Inject] protected IMyService Service { get; set; } = null!;
protected int Count { get; set; }
protected async Task Increment()
{
Count++;
await Service.LogAsync(Count);
}
}
Lifecycle
protected override void OnInitialized() { /* sync init */ }
protected override async Task OnInitializedAsync() { /* async data fetch */ }
protected override void OnParametersSet() { /* khi parameter đổi */ }
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
// JS interop — chỉ chạy 1 lần
await JS.InvokeVoidAsync("initChart", chartId);
}
}
protected override bool ShouldRender() => /* skip render nếu return false */;
public void Dispose() { /* cleanup */ }
Khi dùng cái nào
| Need | Hook |
|---|---|
| Fetch data ban đầu | OnInitializedAsync |
| React khi parent đổi parameter | OnParametersSet(Async) |
| JS interop, focus input, init 3rd-party lib | OnAfterRenderAsync(firstRender) |
| Tối ưu render (skip nếu state không đổi) | ShouldRender |
| Cleanup subscription, timer | IDisposable.Dispose / IAsyncDisposable.DisposeAsync |
Forms — EditForm + Validation
@page "/register"
<EditForm Model="@model" OnValidSubmit="HandleSubmit" FormName="Register">
<DataAnnotationsValidator />
<ValidationSummary />
<InputText @bind-Value="model.Email" />
<ValidationMessage For="@(() => model.Email)" />
<InputText @bind-Value="model.Password" type="password" />
<ValidationMessage For="@(() => model.Password)" />
<button type="submit">Register</button>
</EditForm>
@code {
[SupplyParameterFromForm]
public RegisterModel Model { get; set; } = new();
private RegisterModel model => Model;
public class RegisterModel
{
[Required, EmailAddress] public string Email { get; set; } = "";
[Required, MinLength(8)] public string Password { get; set; } = "";
}
private async Task HandleSubmit()
{
// Submit logic
}
}
[SupplyParameterFromForm]mới có .NET 8+ cho SSR forms. Server-rendered forms hoạt động không cần InteractiveServer.
State Management
| Approach | Use case |
|---|---|
Component state (@code field) |
Local UI state — counter, expand/collapse |
| Cascading parameter | Theme, current user, culture |
| Scoped service (DI) | Cross-component state trong 1 user session (Server) hoặc tab (WASM) |
PersistingComponentStateSubscription |
Persist state qua prerender → interactive transition |
LocalStorage (Blazored.LocalStorage) |
Persist qua page reload (WASM) |
| Fluxor / Redux pattern | App phức tạp, state global rõ ràng |
Service-based state (recommended cho hầu hết case)
public class CartState
{
public List<CartItem> Items { get; private set; } = new();
public event Action? OnChange;
public void Add(CartItem item)
{
Items.Add(item);
OnChange?.Invoke();
}
}
// Program.cs
builder.Services.AddScoped<CartState>(); // Scoped = per circuit (Server) / per tab (WASM)
@inject CartState Cart
@implements IDisposable
<p>Cart count: @Cart.Items.Count</p>
@code {
protected override void OnInitialized() => Cart.OnChange += StateHasChanged;
public void Dispose() => Cart.OnChange -= StateHasChanged;
}
JS Interop
@inject IJSRuntime JS
<input @ref="inputRef" />
@code {
private ElementReference inputRef;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
await inputRef.FocusAsync();
await JS.InvokeVoidAsync("myJs.initChart", "#chart", chartData);
var result = await JS.InvokeAsync<string>("prompt", "Enter name:");
}
}
}
// wwwroot/js/site.js
window.myJs = {
initChart: (selector, data) => { /* ... */ }
};
@* App.razor / _Host.cshtml *@
<script src="js/site.js"></script>
Authentication
@* AuthorizeView component *@
<AuthorizeView>
<Authorized>
<p>Hello @context.User.Identity?.Name</p>
</Authorized>
<NotAuthorized>
<a href="/login">Login</a>
</NotAuthorized>
</AuthorizeView>
@* Page-level *@
@page "/admin"
@attribute [Authorize(Roles = "Admin")]
Anti-patterns
| Anti-pattern | Vấn đề | Fix |
|---|---|---|
Đặt mọi component @rendermode InteractiveServer |
Lãng phí SignalR connection cho component tĩnh | Per-component render mode |
Fetch data trong OnAfterRenderAsync |
Render trước, data sau → flicker | OnInitializedAsync |
Sync HTTP call (.Result) trong WASM |
Deadlock UI thread | await async |
Heavy logic trong @code block markup |
Khó test, mix UI/logic | Code-behind .razor.cs |
Quên Dispose event subscription |
Memory leak (component tree càng dùng càng leak) | IDisposable + unsubscribe |
| Render long list không virtualize | Slow render >500 items | <Virtualize Items="@list"> |
| State trong static field | Shared accidentally giữa users (Server) | Scoped DI service |
| Direct DOM manipulation thay JS interop | Không sync với Blazor render | IJSRuntime.InvokeVoidAsync |
Quên StateHasChanged() sau external event |
UI không update | Gọi StateHasChanged() (hoặc InvokeAsync(StateHasChanged) từ thread khác) |
Performance
@* Virtualize cho list lớn *@
<Virtualize Items="@products" Context="product">
<ProductCard Product="@product" />
</Virtualize>
@* Skip render nếu props không đổi *@
@code {
protected override bool ShouldRender() => _hasChanged;
}
@* Lazy load assembly (WASM) *@
<Router AppAssembly="@typeof(App).Assembly"
AdditionalAssemblies="@lazyLoadedAssemblies"
OnNavigateAsync="OnNavigateAsync">
</Router>
Validate
- Render mode đúng theo nhu cầu (đừng default InteractiveServer cho mọi component)
- Component có
[Parameter]typed, dùngEditorRequiredcho mandatory OnInitializedAsynccho data fetch,OnAfterRenderAsync(firstRender)cho JS interopEditForm+ validator cho mọi form (không tự handle bằng@onsubmit)- Service DI scoped đúng — không static field cho user state
IDisposablecho subscription, timer<Virtualize>cho list >100 items- Cascade values chỉ cho data thực sự cross-cutting (theme, user) — không nghiệp vụ
Hand off to
- Auth pipeline / JWT / cookie config →
aspnet-core - Server-side data →
entity-framework-core - Khi cần API JSON cho mobile/3rd-party →
web-api/minimal-apis - Real-time push beyond Blazor circuit (cross-user notification) →
signalr - C# language features →
modern-csharp
More from nguyenvanchiens/my-skills
gitlab-flow
Standard end-to-end workflow for shipping a feature/bugfix from a Jira task to a merged GitLab MR. Use when the user references a Jira task ID (WRA-XX, etc.), asks to "start a task", "create branch from task", "review the last change", "review the whole branch", "commit and push", "create a merge request", "review the MR !N", "post review result to the MR", "fix all issues", or "merge the request". Covers branch naming, commit format, MR creation, micro + macro code review (3-agent parallel), fix loop, and merge.
14impeccable
Use when the user wants to design, redesign, shape, critique, audit, polish, clarify, distill, harden, optimize, adapt, animate, colorize, extract, or otherwise improve a frontend interface. Covers websites, landing pages, dashboards, product UI, app shells, components, forms, settings, onboarding, and empty states. Handles UX review, visual hierarchy, information architecture, cognitive load, accessibility, performance, responsive behavior, theming, anti-patterns, typography, fonts, spacing, layout, alignment, color, motion, micro-interactions, UX copy, error states, edge cases, i18n, and reusable design systems or tokens. Also use for bland designs that need to become bolder or more delightful, loud designs that should become quieter, live browser iteration on UI elements, or ambitious visual effects that should feel technically extraordinary. Not for backend-only or non-UI tasks.
11karpathy-guidelines
Behavioral guidelines to reduce common LLM coding mistakes. Use when writing, reviewing, or refactoring code to avoid overcomplication, make surgical changes, surface assumptions, and define verifiable success criteria.
10review-branch
Review the cumulative changes of the current branch against main (committed + uncommitted) for reuse, quality, and efficiency, then fix any issues found. Use when finishing a feature branch before opening an MR.
4commit
Commit staged + working-tree changes following Conventional Commits, with the Jira ID as the first token on the subject line. Takes the Jira ID as an argument, e.g. `/commit WRA-9`.
4modern-csharp
Write idiomatic modern C# (C# 12/13/14) trong .NET repos: primary constructors, collection expressions `[..]`, required members, `field` keyword, params collections, lambdas with default params, lock object (.NET 9), extension members. Use khi user hỏi về C# language features, refactor sang syntax mới hơn, kiểm tra LangVersion compat trước khi dùng feature, hoặc review code có pattern cũ (`new()` constructor verbose, ctor + field assignment lặp, raw collection init). Verify TargetFramework và LangVersion trước, không dùng preview feature mặc định.
1