Handle Pre-rendering Right in Blazor: Use Persistent State
April 17, 2025 by Yuriy Frankiv (aka InstanceMaster) · 4 min read
In our previous post Understanding Blazor’s Pre-rendering Behavior, we explored how Blazor’s default pre-rendering behavior can lead to unexpected issues when injecting services, particularly in hybrid (server + WASM) projects. One workaround is to register services on both the server and client, but this can lead to your components rendering twice — once on the server and again on the client during hydration.
To avoid re-running logic and unnecessary performance hits, Blazor provides a mechanism to persist state between the server prerendering and client hydration phases. In this article, we’ll demonstrate the problem and then walk through how to use PersistentComponentState to solve it.
Problem: Inconsistent Service Values Between Server and Client
Imagine you have a shared interface IMyService that provides an initial value for the counter:
public interface IMyService
{
int GetInitialValue();
}
Now, on the server, you register an implementation that returns 100:
public class ServerMyService : IMyService
{
public int GetInitialValue() => 100;
}
On the client, however, your service returns 42:
public class ClientMyService : IMyService
{
public int GetInitialValue() => 42;
}
Both are registered in their respective Program.cs files:
Server
builder.Services.AddScoped<IMyService, ServerMyService>();
Client
builder.Services.AddScoped<IMyService, ClientMyService>();
Then in your component, you inject IMyService and use its value in OnInitialized:
@inject IMyService Service
@code {
private int currentCount;
protected override void OnInitialized()
{
currentCount = Service.GetInitialValue();
}
}
What Happens?
- During server-side prerendering,
ServerMyServicereturns100. - When the app hydrates on the client,
ClientMyServicereturns42.
The user sees the value jump from 100 to 42 as hydration completes.
This example is a bit far-fetched, of course — you'd rarely register completely different implementations like this in real-world apps. But it helps highlight the underlying issue: your logic might be unnecessarily invoked twice, leading to inconsistencies, duplicate API calls, or even race conditions.
This kind of inconsistency can be confusing and hurt user experience.
Solution: Persist the Prerendered State
To prevent the value from being recomputed differently during hydration, we can persist the initial count from the server and reuse it on the client.
Blazor provides PersistentComponentState to help with this.
Here's how we refactor the component using persisted state:
@page "/counter"
@implements IDisposable
@inject IMyService Service
@inject PersistentComponentState ApplicationState
<h1>Counter</h1>
<p role="status">Current count: @currentCount</p>
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
@code {
private int currentCount;
private PersistingComponentStateSubscription persistingSubscription;
protected override void OnInitialized()
{
if (!ApplicationState.TryTakeFromJson<int>(nameof(currentCount), out var restoredCount))
{
currentCount = Service.GetInitialValue();
}
else
{
currentCount = restoredCount;
}
persistingSubscription = ApplicationState.RegisterOnPersisting(PersistCount);
}
private Task PersistCount()
{
ApplicationState.PersistAsJson(nameof(currentCount), currentCount);
return Task.CompletedTask;
}
public void Dispose()
{
persistingSubscription.Dispose();
}
private void IncrementCount()
{
currentCount++;
}
}
Key Concepts
PersistentComponentState
- Available for interactive server or WebAssembly modes.
- Allows you to share state between server prerender and client hydration.
TryTakeFromJson<T>
- Attempts to retrieve and deserialize persisted state.
- If successful, you can skip reinitialization logic on the client.
RegisterOnPersisting
- Registers a callback to store values before the prerendered state is sent.
When Should You Use This?
Use PersistentComponentState when:
- Your component loads data during
OnInitialized. - That data doesn't change between SSR and hydration.
- You want to avoid duplicate logic or visible state changes.
It’s especially useful in performance-sensitive areas or when your SSR logic is expensive or side-effect-prone.
Final Thoughts
Persisting prerendered state is an essential technique for making Blazor’s hybrid rendering experience smoother and more consistent.
That said, I do wish the Blazor team had opted for a simpler or more intuitive default experience. Something less complex than wiring up persistence logic manually would go a long way, especially for newcomers who might not expect their components to render twice out of the box.
If you're using Blazor's new rendering modes and interactive components, combining this pattern with careful service registration can help reduce bugs and boost UX.
Be sure to read the official docs for more details:
🔗 Persist prerendered state (Microsoft Docs)
Got your own tips or examples? I’d love to hear them!
Read Next
Understanding Blazor’s Pre-rendering Behavior: Why Your Service Injection Might Fail
If you’ve been working with Blazor lately, especially using the latest project templates in .NET 8, you might have run into a frustrating and seemingly inexplicable error when injecting services into your components: InvalidOperationException: Cannot provide a value for property 'Service' on type MyPage. There is no registered service of type MyService. You double-check your Program.cs, and your service is clearly registered: builder.Services.AddScoped<MyService>(); So what's going on? Let's break it down.
Read more...
Reduce code boilerplate with Blazor Toolkit. Save ~17 lines per API call
When building real-world Blazor applications, a lot of the complexity doesn't come from UI rendering itself, it comes from state management, service calls, error handling, and validation. Developers often find themselves writing repetitive boilerplate code: toggling loading states, catching exceptions, rolling back model changes, showing validation errors, or paging through API results. The Blazor Toolkit is designed to reduce that friction. By providing a set of reusable components, services, and patterns, it lets you focus more on your business logic and less on wiring code.
Read more...