В чём разница между 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-синхронизации на конкретном примере, а не только определением.
- Указывает последствия для производительности, тестируемости и поддержки.
- Различает документированное поведение текущего стека и устаревшие практики.