BlazorSeniorSystem design

Как управлять состоянием в приложении 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

  • Кандидат объясняет состояние приложения между компонентами, сессиями и запросами на конкретном примере, а не только определением.
  • Указывает последствия для производительности, тестируемости и поддержки.
  • Различает документированное поведение текущего стека и устаревшие практики.

Sources

Related topics