Как управлять состоянием в приложении Blazor?
Управление состоянием в Blazor строится на каскадных параметрах, scoped-сервисах, событиях через Action/EventCallback, PersistentComponentState для SSR и внешних хранилищах (Fluxor, localStorage) для сложных сценариев.
Управление состоянием в Blazor
В Blazor нет встроенного единого решения вроде Redux или MobX — выбор стратегии зависит от области видимости и времени жизни состояния.
1. Локальное состояние компонента
Простейший вариант — поля компонента. Подходит для UI-состояния (открыт/закрыт дропдаун, текущая страница пагинации):
@code {
private bool _isOpen;
private int _page = 1;
void Toggle() => _isOpen = !_isOpen;
}
2. Передача через параметры и EventCallback
Стандартный Blazor-паттерн: данные вниз через [Parameter], события вверх через EventCallback:
@* ParentComponent.razor *@
<ChildComponent Value="_count" OnIncrement="HandleIncrement" />
@code {
private int _count;
void HandleIncrement() => _count++;
}
@* ChildComponent.razor *@
<button @onclick="() => OnIncrement.InvokeAsync()">+</button>
@code {
[Parameter] public int Value { get; set; }
[Parameter] public EventCallback OnIncrement { get; set; }
}
3. Каскадные параметры
Используются для состояния, которое нужно передать глубоко вниз по дереву (тема, локаль, аутентификация):
@* App.razor *@
<CascadingValue Value="_theme">
<Router />
</CascadingValue>
@code { private string _theme = "dark"; }
@* Любой дочерний компонент *@
@code {
[CascadingParameter] private string Theme { get; set; } = "light";
}
4. Scoped-сервис как State Container
Наиболее гибкий встроенный подход. Scoped-сервис живёт в рамках одной сессии (Server) или вкладки (WASM):
// AppState.cs
public class AppState
{
public int CartItemCount { get; private set; }
public event Action? OnChange;
public void AddToCart()
{
CartItemCount++;
OnChange?.Invoke();
}
}
// Program.cs
builder.Services.AddScoped<AppState>();
@* CartIcon.razor *@
@implements IDisposable
@inject AppState State
<span>@State.CartItemCount</span>
@code {
protected override void OnInitialized() =>
State.OnChange += StateHasChanged;
public void Dispose() =>
State.OnChange -= StateHasChanged;
}
5. PersistentComponentState (SSR → Interactive)
При смешанном рендеринге (Static SSR + Interactive) данные нужно передать из SSR-фазы в интерактивную, чтобы избежать повторного запроса:
@inject PersistentComponentState AppState
@implements IDisposable
@code {
private PersistingComponentStateSubscription _subscription;
private List<Product>? _products;
protected override async Task OnInitializedAsync()
{
_subscription = AppState.RegisterOnPersisting(Persist);
if (!AppState.TryTakeFromJson<List<Product>>("products", out _products))
{
_products = await ProductService.GetAllAsync();
}
}
private Task Persist()
{
AppState.PersistAsJson("products", _products);
return Task.CompletedTask;
}
public void Dispose() => _subscription.Dispose();
}
6. Fluxor — Redux-подобное решение
// Установка
dotnet add package Fluxor.Blazor.Web
// State
[FeatureState]
public record CounterState(int Count = 0);
// Action
public record IncrementAction;
// Reducer
public static class CounterReducers
{
[ReducerMethod]
public static CounterState Reduce(CounterState state, IncrementAction _)
=> state with { Count = state.Count + 1 };
}
Подводные камни
- В Blazor Server Scoped-сервис живёт на протяжении всей SignalR-сессии — не храните в нём большие объекты, иначе память сервера растёт без освобождения.
- Подписка на
OnChangeбез отписки вDispose— классическая утечка памяти в Blazor; всегда реализуйтеIDisposable. - Каскадные параметры по умолчанию сравниваются по ссылке — если передавать value-type или неизменяемую запись, используйте
IsFixed="true"для оптимизации рендеринга. - В WASM Scoped-сервис фактически является Singleton на время жизни вкладки — проектируйте State Container с учётом этого.
PersistentComponentStateданные сериализуются в HTML-страницу как JSON — не сохраняйте чувствительные данные (токены, пароли).- Fluxor удобен для крупных приложений, но для небольших SPA добавляет избыточную сложность — начинайте с State Container и мигрируйте по необходимости.
Common mistakes
- Путать состояние приложения между компонентами, сессиями и запросами с похожим механизмом из другой версии или платформы.
- Игнорировать runtime-границы Blazor: lifecycle, DI scope, SQL translation, UI thread или platform API.
- Не обсуждать null/empty/error cases и поведение под нагрузкой.
What the interviewer is testing
- Кандидат объясняет состояние приложения между компонентами, сессиями и запросами на конкретном примере, а не только определением.
- Указывает последствия для производительности, тестируемости и поддержки.
- Различает документированное поведение текущего стека и устаревшие практики.