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 để nghe thay đổi.
🧬 6. Nâng cấp: Generic StateContainer
Khi app lớn, bạn sẽ có UserState, CartState, GameState… Tránh lặp code bằng base class:
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;
OnChange?.Invoke();
}
protected void UpdateState(Func<TState, TState> update)
{
_state = update(_state);
OnChange?.Invoke();
}
}
⚠️ Lưu ý constraintwhere TState : new()— bắt buộc phải có, không thì compiler sẽ báo lỗi ngay vìnew()trong generic cần guaranteeTStatecó parameterless constructor.
Ví dụ:
public record UserStateData
{
public string? Username { get; init; }
public bool IsAuthenticated { get; init; }
}
public class UserState : StateContainerBase<UserStateData>
{
public void Login(string username)
{
SetState(new UserStateData
{
Username = username,
IsAuthenticated = true
});
}
}
💾 7. Persistence – Giữ state sau khi refresh
❗ Vấn đề cũ: Refresh trang là… reset hết, giỏ hàng bay màu, counter về 0, user phải login lại. Đau vl 🍜.
✅ Từ .NET 10, Blazor có attribute [PersistentState] — cực kỳ gọn:
@code {
[PersistentState]
public int Count { get; set; } = 0;
void Increment() => Count++;
}
Chỉ vậy thôi. Refresh xong vẫn giữ nguyên số đếm. Blazor tự lo phần serialize/deserialize, không cần đụng vào localStorage hay sessionStorage bằng tay.
📌[PersistentState]là feature mới từ .NET 10. Nếu bạn đang dùng .NET 9 trở xuống thì phải làm thủ công hơn quaPersistentComponentStatehoặc thư việnBlazored.LocalStorage.
🧩 8. Khi app to 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