LEARN · DEBUGGING GUIDE

Blazor Component Not Re-rendering After StateHasChanged Call

If your Blazor component is not re-rendering after StateHasChanged, the issue is almost never that you forgot to call it. It's threading, render tree mismatch, or parameter immutability. This guide shows you how to find which.

IntermediateDotnet6 min read

What this usually means

StateHasChanged requests a re-render but Blazor's renderer can skip it if it detects no relevant changes. The most common causes: calling StateHasChanged from a non-UI thread without InvokeAsync, passing immutable objects as parameters where the reference hasn't changed, or the component's render tree being identical to the previous frame (diff skipped). Also, if the component is disposed or not attached to the renderer, the call is silently ignored.

( 01 )Fast diagnosis

The first ten minutes — establish facts before touching code.

  • 1Check if StateHasChanged is called from a background thread or Task continuation without InvokeAsync — add InvokeAsync(StateHasChanged) and test
  • 2Log the component's RenderCount in OnAfterRender and verify increments match expected calls
  • 3Override ShouldRender() to return true always — if that fixes it, the issue is render suppression
  • 4Inspect parameter objects: if they are mutable classes, ensure the reference changes (use new object or records)
  • 5Check if the component is wrapped in a RenderFragment that is conditionally rendered or has a key that changes
  • 6Add a console.warn in Dispose() to confirm the component isn't being torn down prematurely
( 02 )Where to look

The specific files, logs, configs, and dashboards that usually own this bug.

  • searchComponent code: all StateHasChanged call sites
  • searchParent component: how parameters are passed (immutable? new reference each time?)
  • searchEvent callback handlers: are they async void? Do they run on SynchronizationContext?
  • searchBrowser DevTools console: unhandled exceptions in render tree
  • searchBlazor Server circuit logs (if server-side): check for circuit disconnection or reconnection
  • searchThird-party JavaScript interop: does it modify DOM outside Blazor's knowledge?
( 03 )Common root causes

Practical causes, not theory. These are the things you will actually find.

  • warningStateHasChanged called on a background thread without InvokeAsync
  • warningParameter objects are mutable but same reference passed each render → Blazor skips update
  • warningShouldRender() returns false due to custom logic
  • warningComponent is inside a RenderFragment that is conditionally rendered and the condition hasn't changed
  • warningComponent is disposed or removed from the render tree (e.g., key changed on parent)
  • warningAsync void event handlers swallow exceptions and never trigger re-render
( 04 )Fix patterns

Concrete fix directions. Pick the one that matches your root cause.

  • buildWrap StateHasChanged calls in InvokeAsync(() => StateHasChanged()) when off the UI thread
  • buildUse immutable parameters: records, or ensure you create a new object instance on each update
  • buildOverride ShouldRender() to return true if you need unconditional re-renders, or add logic to detect changes properly
  • buildFor async event handlers, use async Task instead of async void to let Blazor track the completion
  • buildIf using RenderFragment with conditional rendering, ensure the key attribute changes to force component rebuild
( 05 )How to verify

A fix you cannot prove is a guess. Close the loop.

  • verifiedAdd a log line in OnAfterRender(true) with a counter; confirm it increments on each expected update
  • verifiedRemove ShouldRender override entirely and see if re-renders happen
  • verifiedTemporarily pass a Guid.NewGuid() as a parameter to force new reference — if UI updates, the issue is parameter immutability
  • verifiedCall InvokeAsync(StateHasChanged) even from UI thread — if it fixes, the issue was sync context
  • verifiedCheck with browser DevTools that the component's DOM subtree is actually replaced (not just internal state)
( 06 )Mistakes to avoid

Things that make this bug worse or harder to find.

  • warningDon't call StateHasChanged in OnInitializedAsync before the first render completes — it's redundant and can cause double renders
  • warningDon't assume StateHasChanged is synchronous; it only queues a render request
  • warningDon't mutate objects in place and expect Blazor to detect changes — Blazor does not deep compare
  • warningDon't wrap the entire component in a conditional RenderFragment that hides/recreates it without key management
  • warningDon't ignore the browser console — exceptions in render tree can silently stop re-renders
( 07 )War story

Customer Dashboard Stale After WebSocket Update

Senior Backend EngineerBlazor Server .NET 6, SignalR, Dapper, SQL Server

Timeline

  1. 09:15User reports dashboard not updating after new order placed
  2. 09:20Check logs: SignalR event received by server, hub broadcasts to clients
  3. 09:25In Blazor component: OnOrderUpdated handler calls StateHasChanged
  4. 09:30Add debug log in OnAfterRender: counter increments only on first load
  5. 09:35Notice OnOrderUpdated is async void, wrapped in Task.Run in hub client
  6. 09:40Change to async Task, add InvokeAsync around StateHasChanged
  7. 09:45Dashboard now updates live. Root cause: background thread without InvokeAsync

I was working on a real-time dashboard for customer orders. The Blazor Server component subscribed to a SignalR hub that broadcasted new orders. When an order came in, the hub invoked a callback on the client, which called StateHasChanged. But the UI didn't update.

I spent 20 minutes adding logs. The callback was definitely firing. I saw the data in memory had changed. But OnAfterRender only ran once. Then I noticed: the callback method was async void and inside it I had a Task.Run to do some CPU work. StateHasChanged was called from that background thread.

Blazor's renderer requires all UI updates on the SynchronizationContext. I changed the callback to async Task and wrapped StateHasChanged in InvokeAsync. Instant fix. The lesson: never call StateHasChanged from a thread pool thread without InvokeAsync.

Root cause

StateHasChanged called from a background thread (Task.Run) without InvokeAsync, so the render request was silently ignored.

The fix

Changed the event handler from async void to async Task, and wrapped StateHasChanged in InvokeAsync(() => StateHasChanged()).

The lesson

Always marshal UI updates to the Blazor SynchronizationContext using InvokeAsync, even if you think you're on the UI thread.

( 08 )Understanding Blazor's Render Pipeline and StateHasChanged

StateHasChanged is not a direct DOM update. It marks the component as needing a re-render and schedules a render. The renderer then executes the component's BuildRenderTree method and diffs the output against the previous render tree. If the diff is empty (identical trees), no DOM changes are applied.

The renderer runs on the Blazor SynchronizationContext. In Blazor Server, this is the same as the ASP.NET Core SynchronizationContext. In Blazor WebAssembly, it's the browser's main thread. Calling StateHasChanged from any other thread without InvokeAsync will queue the render on the wrong context, and it may never execute.

( 09 )Parameter Immutability and Render Suppression

Blazor uses reference equality for parameters to decide if a re-render is needed. If you pass a mutable object (like a List<T> or a custom class) and only modify its properties, the reference remains the same. Blazor sees the same parameter instance and may skip the child component's render entirely.

The fix is to use immutable objects or ensure you create a new instance when data changes. Records in C# 9+ are perfect because they have value-based equality. Alternatively, override ShouldRender in the child component to compare the actual data.

( 10 )The async void Pitfall

Event handlers in Blazor that are async void are fire-and-forget. Blazor cannot track their completion, so it won't automatically call StateHasChanged after they finish. Worse, exceptions thrown in async void methods crash the process (in Blazor Server) or go unhandled.

Always use async Task for event handlers. Blazor will wait for the task and trigger a re-render when it completes. If you need to update the UI during the async operation (e.g., show a loading spinner), call StateHasChanged manually at the right points.

( 11 )Conditional RenderFragments and Key Management

If your component is inside a RenderFragment that is conditionally rendered (e.g., @if (showComponent)), and the condition doesn't change, the component won't be re-rendered even if its internal state changes. The parent controls the existence, not the updates.

Use the @key directive to give Blazor a hint to destroy and recreate the component when the key changes. But be careful: recreating destroys state. Alternatively, lift the state up to the parent and pass it as parameters that change.

( 12 )Debugging Tools: Render Counting and Logging

The quickest way to confirm a render is happening is to add a counter in OnAfterRender: @code { int _renderCount; protected override void OnAfterRender(bool firstRender) { _renderCount++; Console.WriteLine($"Render #{_renderCount}"); } }. If the counter doesn't increment when you expect, the render is not being triggered.

You can also override ShouldRender to return true always and see if the problem persists. If the UI updates with ShouldRender forced true, then the render suppression logic is too aggressive. Also check the browser console for any exceptions in the render tree—they can abort rendering silently.

Frequently asked questions

Why does InvokeAsync(StateHasChanged) work but direct StateHasChanged doesn't?

Direct StateHasChanged from a non-UI thread queues the render on the wrong synchronization context. InvokeAsync marshals the call to the Blazor SynchronizationContext, ensuring the render request is processed correctly.

Can StateHasChanged be called too often? Does it cause performance issues?

StateHasChanged is cheap; it only marks the component for render. The actual render (BuildRenderTree and diff) happens at most once per frame (typically every ~16ms in WASM, or per circuit task in Server). Calling it many times in a loop will only result in one render. However, avoid calling it in tight loops that also trigger DOM changes (like inside OnInput).

My component updates when I click a button but not when data comes from a timer. Why?

Timer callbacks run on the thread pool, not the Blazor SynchronizationContext. You must call InvokeAsync(StateHasChanged) inside the timer callback. Also, ensure the timer is not disposed or the component is still alive (use CancellationToken).

ShouldRender() returns false by default? Do I need to override it?

No, ShouldRender() is not overridden by default and returns true. The render suppression happens inside the framework based on parameter equality and other heuristics. Only override ShouldRender if you have custom logic to skip renders.

Does StateHasChanged work during OnInitialized?

It works but is unnecessary because Blazor will automatically re-render after OnInitializedAsync completes. Calling StateHasChanged during initialization can cause an extra render cycle. It's harmless but redundant.