PersistentComponentState: Surviving Blazor’s Prerender-to-Interactive Handoff

Blazor Server and Blazor Auto both support prerendering: the server renders your page to HTML on the first request so the user sees content immediately. Then the interactive runtime boots, components re-initialize, and OnInitializedAsync runs again. Your API calls fire a second time for data you already had moments ago.

The result? A flash of empty content, wasted resources, and double the cost for metered APIs.

The Problem

@code {
private Product[]? products;
protected override async Task OnInitializedAsync()
{
// Runs TWICE: once during prerender, once when interactive
products = await Http.GetFromJsonAsync<Product[]>("/api/products");
}
}

During prerender, the data loads and renders into HTML. Then the interactive runtime starts, products resets to null, “Loading…” flashes, and the fetch runs again.

The Fix (Manual Approach)

Inject PersistentComponentState to stash data during prerender and restore it on interactive boot:

@inject PersistentComponentState ApplicationState
@implements IDisposable
@code {
private Product[]? products;
private PersistingComponentStateSubscription _subscription;
protected override async Task OnInitializedAsync()
{
_subscription = ApplicationState.RegisterOnPersisting(PersistData);
if (!ApplicationState.TryTakeFromJson<Product[]>(
"products",
out var restored))
restored = await Http.GetFromJsonAsync<Product[]>("/api/products");
products = restored;
}
private Task PersistData()
{
ApplicationState.PersistAsJson("products", products);
return Task.CompletedTask;
}
public void Dispose() => _subscription.Dispose();
}

On the prerender pass, TryTakeFromJson returns false, so the fetch runs. Blazor serializes the data into a hidden element in the HTML. On the interactive boot, TryTakeFromJson returns true with the data. No second fetch.

The Fix (.NET 10): The [PersistentState] Attribute

.NET 10 simplifies this to a one-liner:

@code {
[PersistentState]
public Product[]? Products { get; set; }
protected override async Task OnInitializedAsync()
{
Products ??= await Http.GetFromJsonAsync<Product[]>("/api/products");
}
}

The [PersistentState] attribute handles serialization, persistence, and restoration automatically. The ??= check ensures the API call only runs when the data wasn’t already restored.

Gotchas

  • Keep it small. The state is serialized as JSON and embedded in the HTML. A few records are fine. Thousands of rows are not.
  • Keys must be unique. The property name (or string key in the manual approach) must be unique across all components on the page.
  • Not a general cache. It only transfers state from the prerender pass to the interactive boot. It doesn’t persist across navigations or page reloads.

Key Takeaway

PersistentComponentState eliminates the double-fetch problem in Blazor prerendering. In .NET 10, [PersistentState] makes it a one-liner. If your components flash “Loading…” after the page already rendered with data, this is the fix.