Handle Pre-rendering Right in Blazor: Use Persistent State

Handle Pre-rendering Right in Blazor: Use Persistent State

4/17/2025 by InstanceMaster

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, ServerMyService returns 100.
  • When the app hydrates on the client, ClientMyService returns 42.

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!