BlazorSeniorTechnical

В чём разница между InvokeAsync(StateHasChanged) и StateHasChanged()?

StateHasChanged() должна вызываться из UI-потока; InvokeAsync(StateHasChanged) маршалирует вызов в контекст синхронизации компонента и безопасен из фоновых потоков и таймеров.

StateHasChanged vs InvokeAsync(StateHasChanged)

Как работает StateHasChanged

Метод StateHasChanged() уведомляет Blazor о том, что состояние компонента изменилось и необходим повторный рендеринг. В Blazor Server он выполняется в контексте синхронизации, связанном с SignalR-соединением конкретного пользователя. В Blazor WebAssembly — в основном потоке браузера.

Если StateHasChanged() вызывается из другого потока (таймер, фоновая задача, обработчик события из внешнего сервиса), это нарушает потокобезопасность и в Blazor Server приводит к исключению или повреждённому состоянию рендеринга.

InvokeAsync — безопасный маршалинг

InvokeAsync — метод базового класса ComponentBase, унаследованный от IHandleEvent. Он выполняет делегат в контексте синхронизации компонента, аналогично Dispatcher.InvokeAsync в WPF или Control.Invoke в WinForms.

// ПРАВИЛЬНО: вызов из фонового потока
private Timer? _timer;

protected override void OnInitialized()
{
    _timer = new Timer(async _ =>
    {
        _counter++;
        await InvokeAsync(StateHasChanged); // безопасно из ThreadPool
    }, null, TimeSpan.Zero, TimeSpan.FromSeconds(1));
}

public async ValueTask DisposeAsync()
{
    if (_timer is not null)
        await _timer.DisposeAsync();
}
// НЕПРАВИЛЬНО: прямой вызов из другого потока в Blazor Server
private void OnExternalEvent(object? sender, EventArgs e)
{
    _data = "updated";
    StateHasChanged(); // может выбросить InvalidOperationException
}

Когда StateHasChanged() безопасна без InvokeAsync

  • Внутри обработчиков событий компонента (@onclick, @oninput) — Blazor уже находится в правильном контексте.
  • Внутри OnInitializedAsync, OnParametersSetAsync — вызываются из корректного контекста.
  • После await внутри жизненного цикла компонента — SynchronizationContext сохраняется.

Паттерн с внешними сервисами

@implements IDisposable
@inject INotificationHub Hub

@code {
    private string _lastMessage = "";

    protected override void OnInitialized()
    {
        Hub.OnMessage += HandleMessage;
    }

    private async void HandleMessage(string message)
    {
        _lastMessage = message;
        await InvokeAsync(StateHasChanged);
    }

    public void Dispose()
    {
        Hub.OnMessage -= HandleMessage;
    }
}

Обратите внимание: async void допустим только для обработчиков событий — исключения из него не перехватываются вызывающей стороной.

InvokeAsync с произвольным действием

// Выполнить любой код + обновить UI атомарно
await InvokeAsync(() =>
{
    _items = newItems;
    StateHasChanged();
});

Подводные камни

  • В Blazor WebAssembly всё работает в одном потоке, поэтому проблемы потокобезопасности не возникают — но код с InvokeAsync остаётся переносимым и корректен для Blazor Server.
  • StateHasChanged() внутри OnAfterRenderAsync вызывает ещё один цикл рендеринга — легко создать бесконечный цикл, если не добавить флаг firstRender.
  • Частый вызов InvokeAsync(StateHasChanged) из таймера с маленьким интервалом нагружает SignalR-канал — добавляйте дебаунс или ограничение частоты.
  • После Dispose компонента вызов InvokeAsync выбрасывает ObjectDisposedException — проверяйте флаг _disposed перед вызовом.
  • При наследовании от ComponentBase метод InvokeAsync доступен напрямую; при наследовании от OwningComponentBase или кастомных классов убедитесь, что он также доступен.

Common mistakes

  • Путать InvokeAsync(StateHasChanged) и поток UI-синхронизации с похожим механизмом из другой версии или платформы.
  • Игнорировать runtime-границы Blazor: lifecycle, DI scope, SQL translation, UI thread или platform API.
  • Не обсуждать null/empty/error cases и поведение под нагрузкой.

What the interviewer is testing

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

Sources

Related topics