State trong Blazor là một trong những khái niệm quan trọng nhất. Nếu hiểu đúng, bạn sẽ thấy Blazor rất "mượt"; nếu hiểu sai, code sẽ nhanh chóng thành… mì gói 🍜.
Bài này đi từ ví dụ đơn giản nhất (click button) đến các pattern nâng cao như StateContainer, PersistentState, Fluxor và Undo/Redo.
1. State là gì?
State = dữ liệu hiện tại của ứng dụng tại một thời điểm.
Ví dụ:
- Counter = 5
- User đã đăng nhập
- Giỏ hàng có 3 sản phẩm
UI luôn được render dựa trên state.
2. Ví dụ cơ bản nhất: Click Counter
Counter.razor
<h3>Counter</h3>
<p>Current count: @count</p>
<button @onclick="Increment">Click me</button>
@code {
int count = 0;
void Increment()
{
count++;
}
}
Điều gì đang xảy ra? Flow rất đơn giản:
User click
↓
Increment()
↓
count thay đổi
↓
Blazor re-render UI
Đây chính là nền tảng: UI = f(state)
3. Khi state nằm trong component
Cách trên hoạt động tốt khi:
- State chỉ dùng trong 1 component
- Không cần chia sẻ
Nhưng vấn đề xuất hiện khi:
Component A cần state
Component B cũng cần
Component C cũng cần
4. Cách sai (truyền Parameter lòng vòng)
A → B → C → D
Code sẽ:
- khó maintain
- coupling cao
- dễ bug
🧠 5. Giải pháp: StateContainer
📌 Ý tưởng: Tạo một service giữ state và thông báo khi state thay đổi.
AppState.cs
public class AppState
{
private int _count;
public int Count
{
get => _count;
set
{
_count = value;
NotifyStateChanged();
}
}
public event Action? OnChange;
private void NotifyStateChanged() => OnChange?.Invoke();
}
Program.cs
builder.Services.AddSingle<AppState>();
Tất cả mọi người vào web của bạn đều dùng chung một biến. Cách này khá nguy hiểm.
Component sử dụng
@inject AppState AppState
@implements IDisposable
<p>Count: @AppState.Count</p>
<button @onclick="Increase">Click</button>
@code {
protected override void OnInitialized()
{
AppState.OnChange += HandleChange;
}
void Increase()
{
AppState.Count++;
}
void HandleChange()
{
InvokeAsync(StateHasChanged);
}
public void Dispose()
{
AppState.OnChange -= HandleChange;
}
}
🎯 Ý nghĩa: Service giữ state, Components subscribe để lắng nghe thay đổi.
Circuit là gì?
Circuit trong Blazor Interactive Server giống như một cuộc gọi điện thoại liên tục (kết nối SignalR) giữa trình duyệt của bạn và server.
Mỗi tab trình duyệt sẽ có một Circuit riêng. Circuit chỉ bị hủy khi đóng tab hoặc refresh trang.
@page "/counter"
@rendermode InteractiveServer
6. Nâng cấp: Generic StateContainerBase
Khi app bắt đầu lớn lên, bạn sẽ không muốn tạo từng class AppState lặp đi lặp lại cho từng loại state (UserState, CartState, ThemeState…). Lúc này, StateContainerBase sẽ giúp bạn viết code sạch hơn, ít lặp lại hơn và dễ maintain hơn.
Ý tưởng chính:
- Dùng một class base (abstract) để xử lý chung việc lưu state và phát event thông báo thay đổi.
- Mỗi loại state sẽ kế thừa từ
StateContainerBase<TState>, trong đóTStatethường là mộtrecordhoặc class immutable. - Khuyến khích dùng immutable (record) để tránh mutate state trực tiếp – giúp code an toàn và dễ debug hơn.
Ví dụ thực tế: CounterState
Đầu tiên, ta định nghĩa class chứa dữ liệu (nên dùng record để immutable):
public record CounterState
{
public int Count { get; init; } = 0;
}
Sau đó tạo class AppState kế thừa từ StateContainerBase:
public class AppState : StateContainerBase<CounterState>
{
public int Count
{
get => _state.Count;
set => UpdateState(state => state with { Count = value });
}
public void Increment()
{
UpdateState(state => state with { Count = state.Count + 1 });
}
public void Decrement()
{
UpdateState(state => state with { Count = state.Count - 1 });
}
public void Reset()
{
UpdateState(_ => new CounterState { Count = 0 });
}
}
StateContainerBase là gì?
Đây là lớp abstract giúp xử lý phần chung cho tất cả các StateContainer:
public abstract class StateContainerBase<TState> where TState : new()
{
protected TState _state = new();
public event Action? OnChange;
public TState State => _state;
protected void SetState(TState newState)
{
_state = newState;
NotifyStateChanged();
}
protected void UpdateState(Func<TState, TState> update)
{
_state = update(_state);
NotifyStateChanged();
}
protected void NotifyStateChanged() => OnChange?.Invoke();
}
Cách sử dụng trong Component
Sử dụng hoàn toàn giống như AppState thông thường:
@inject AppState AppState
@implements IDisposable
<p>Current count: @AppState.Count</p>
<button @onclick="AppState.Increment">+1</button>
<button @onclick="AppState.Decrement">-1</button>
<button @onclick="AppState.Reset">Reset</button>
@code {
protected override void OnInitialized()
{
AppState.OnChange += HandleStateChanged;
}
private void HandleStateChanged()
{
InvokeAsync(StateHasChanged);
}
public void Dispose()
{
AppState.OnChange -= HandleStateChanged;
}
}
Lợi ích khi dùng StateContainerBase:
- Không phải viết lại code notify event, SetState, UpdateState cho mỗi loại state.
- Dễ mở rộng (thêm UserState, CartState… chỉ cần kế thừa).
- Khuyến khích dùng immutable (record +
withexpression) → code an toàn hơn. - Dễ test hơn vì logic nằm hoàn toàn ở service, không phụ thuộc UI.
7. PersistentState: Khắc phục Flash khi Pre-rendering
Flash là gì?
Flash là hiện tượng chớp nhoáng mà bạn thấy trên trang Blazor khi component được render hai lần:
- Server chạy: Tạo dữ liệu, render HTML
- Client chạy: Chạy lại code, tạo dữ liệu mới khác
- Hai dữ liệu xung đột: Trang nháy một cái
Ví dụ: Trang thời tiết hiển thị 40°F → Flash ⚡ → 27°F
Cách cũ
Trước .NET 10, developer tắt pre-rendering:
@rendermode @(new InteractiveServerRenderMode(false))
Vấn đề:
- Trang tải chậm hơn
- HTML trống lúc đầu
- SEO kém
- Người dùng thấy "Loading..."
Giải pháp mới: [PersistentState]
Attribute [PersistentState] cho phép Blazor lưu giữ dữ liệu từ server render và tái sử dụng khi client khởi động.
Kết quả: Không flash, không tải lại, trang hiển thị mượt mà.
Cách sử dụng
@page "/weather"
@rendermode InteractiveServer
@if (Forecasts == null)
{
<p><em>Loading...</em></p>
}
else
{
<table class="table">
<thead>
<tr>
<th>Date</th>
<th>Temperature (C)</th>
<th>Summary</th>
</tr>
</thead>
<tbody>
@foreach (var forecast in Forecasts)
{
<tr>
<td>@forecast.Date.ToShortDateString()</td>
<td>@forecast.TemperatureC</td>
<td>@forecast.Summary</td>
</tr>
}
</tbody>
</table>
}
@code {
[PersistentState]
public WeatherForecast[]? Forecasts { get; set; }
protected override async Task OnInitializedAsync()
{
await Task.Delay(500);
// only create new data if not existing data
if (Forecasts == null)
{
var startDate = DateOnly.FromDateTime(DateTime.Now);
var summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild",
"Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};
Forecasts = Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
Date = startDate.AddDays(index),
TemperatureC = Random.Shared.Next(-20, 55),
Summary = summaries[Random.Shared.Next(summaries.Length)]
}).ToArray();
}
}
public class WeatherForecast
{
public DateOnly Date { get; set; }
public int TemperatureC { get; set; }
public string? Summary { get; set; }
}
}
Điều gì xảy ra?
Lần 1 (Server - Pre-rendering):
- Forecasts = null
- if (null) → TRUE
- Tạo dữ liệu: [25°C, 30°C, 35°C]
- Render HTML
- Lưu dữ liệu vào PersistentState
Lần 2 (Client - Hydration):
- PersistentState khôi phục: Forecasts = [25°C, 30°C, 35°C]
- if (null) → FALSE
- Bỏ qua tạo dữ liệu
- Render HTML với cùng dữ liệu
- Không flash!
Tại sao phải check null?
Nếu không check, code sẽ luôn tạo dữ liệu mới lần 2, ghi đè dữ liệu từ server:
protected override async Task OnInitializedAsync()
{
Forecasts = Enumerable.Range(1, 5)... // Luôn tạo mới, ghi đè
}
Kết quả: Server: 25°C → Flash ⚡ → Client: 18°C (random mới)
Với check null:
if (Forecasts == null)
Forecasts = Enumerable.Range(1, 5)...
Kết quả: Server: 25°C → Client: 25°C (không flash) ✅
Tính năng nâng cao
1. Cho phép cập nhật khi điều hướng
[PersistentState(AllowUpdates = true)]
public WeatherForecast[]? Forecasts { get; set; }
Giờ khi bạn chuyển từ trang này sang trang khác, dữ liệu vẫn giữ nguyên.
2. Kiểm soát hành vi khôi phục
[PersistentState(AllowUpdates = true, RestoreBehavior = RestoreBehavior.SkipLastSnapshot)]
public WeatherForecast[]? Forecasts { get; set; }
Các tuỳ chọn:
Default: Khôi phục tất cả dữ liệuSkipInitialValue: Bỏ qua dữ liệu pre-render lần đầuSkipLastSnapshot: Bỏ qua snapshot cuối cùng (luôn tải mới)
Khi nào dùng PersistentState?
- 🛒 E-commerce: Phải trông chuyên nghiệp, không flash
- 📊 Dashboard: Dữ liệu real-time cần ổn định
- 📱 Blazor Hybrid: Kết hợp server + client mode
- 📰 Blog/News: Dữ liệu tĩnh, cần SEO tốt
8. Khi app lớn hơn: Nên cân nhắc Fluxor
StateContainer + Generic như trên rất ổn cho app vừa và nhỏ. Nhưng khi dự án phình to, hàng chục state khác nhau, action phức tạp, thì nhiều team chuyển sang Fluxor — thư viện Flux/Redux cho .NET.
Fluxor hoạt động theo kiểu unidirectional data flow rõ ràng: Action → Reducer → State → UI.
Cài nhanh:
dotnet add package Fluxor.Blazor.Web
Ví dụ nhanh — define một counter feature:
// State
public record CounterState(int Count);
// Action
public record IncrementAction();
// Reducer
public static class CounterReducers
{
[ReducerMethod]
public static CounterState OnIncrement(CounterState state, IncrementAction _)
=> state with { Count = state.Count + 1 };
}
Dùng trong component:
@inject IState<CounterState> CounterState
@inject IDispatcher Dispatcher
<p>Count: @CounterState.Value.Count</p>
<button @onclick='() => Dispatcher.Dispatch(new IncrementAction())'>Click</button>
Ưu điểm lớn:
- Code rất dễ test (không dính UI)
- Predictable, dễ debug và trace lỗi
- Hỗ trợ middleware, effect, persistence tốt
- Ít boilerplate hơn so với Redux JS ngày xưa
Nếu bạn thấy StateContainer đang "lộn xộn" thì Fluxor là bước nâng cấp tự nhiên đấy.
⏪ 9. Undo / Redo
🧠 Ý tưởng: Dùng 2 stack (Undo và Redo).
SetState → push undo, clear redo
Undo → pop undo, push redo
Redo → pop redo, push undo
Implementation skeleton:
public class UndoableState<TState> where TState : new()
{
private TState _current = new();
private readonly Stack<TState> _undoStack = new();
private readonly Stack<TState> _redoStack = new();
public TState Current => _current;
public event Action? OnChange;
public void SetState(TState newState)
{
_undoStack.Push(_current);
_redoStack.Clear();
_current = newState;
OnChange?.Invoke();
}
public void Undo()
{
if (_undoStack.Count == 0) return;
_redoStack.Push(_current);
_current = _undoStack.Pop();
OnChange?.Invoke();
}
public void Redo()
{
if (_redoStack.Count == 0) return;
_undoStack.Push(_current);
_current = _redoStack.Pop();
OnChange?.Invoke();
}
}
Ứng dụng: editor, game (cờ vua cực hợp), form phức tạp.
🧪 10. Testing (điểm mạnh lớn)
StateContainer không phụ thuộc UI nên test dễ:
var state = new CartState();
state.AddItem(new CartItem { Name = "Laptop", Price = 999 });
Assert.Equal(1, state.State.Items.Count);
Assert.Equal(999, state.State.TotalPrice);
⚠️ 11. Best Practices
✅ Nên:
- Dùng immutable (
record) - Expose read-only hoặc qua method
- Unsubscribe event trong
Dispose - Dùng
InvokeAsync(StateHasChanged) - Nhớ thêm
where TState : new()khi dùng Generic StateContainer - Dùng
[PersistentState]khi có thể (.NET 10+) - Cân nhắc Fluxor cho app phức tạp
❌ Tránh:
- Dùng
static - Sửa state trực tiếp từ bên ngoài
- Quên
Dispose - Gọi
StateHasChangedsai context
🏁 12. Kết luận
Blazor State đi theo hướng rất rõ:
User action
↓
Update state
↓
Notify
↓
UI re-render
Lộ trình học:
- Level 1: State trong component
- Level 2: StateContainer
- Level 3: Generic + clean architecture
- Level 3.5:
[PersistentState](gần như không tốn công) - Level 4: Fluxor hoặc Undo/Redo
👉 Nếu bạn đã từng làm ASP.NET: Blazor không "giả lập state" nữa mà cho bạn làm việc với state thật (object trong memory).
👉 Nếu bạn từng dùng React: Bạn đã hiểu 70% Blazor rồi.
🚀 Gợi ý tiếp theo: Thử áp dụng vào Todo App, Shopping Cart hoặc Chess Game. Bạn sẽ thấy StateContainer (và Fluxor) phát huy sức mạnh rất rõ.
Nhận xét
Đăng nhận xét