- Lab
-
Libraries: If you want this lab, consider one of these libraries.
- Core Tech
Guided: Managing Data Flow, Hydration, and Persisted State in Blazor
When Blazor mixes Static SSR with interactive render modes, data fetched on the server disappears at the client boundary, components re-fetch data they already have, and the UI flickers after it appeared ready. In this Guided Code Lab, you will work inside the Globomantics Store to trace how Blazor moves data across render phases, implement persisted state to eliminate redundant API calls, and diagnose common hydration issues.
Lab Info
Table of Contents
-
Challenge
Introduction
In this lab you will work inside the Globomantics Store, a product catalog application built with Blazor. You will trace how Blazor moves data from server to client across render phases, implement persisted state to eliminate a redundant API call, and fix real-world hydration issues that surface when render modes are mixed.
Scenario
You are a .NET developer at Globomantics. The team has built the Globomantics Store, a product catalog and shopping experience using Blazor, combining Static SSR for fast initial page load with Interactive Server for interactivity. The application works correctly when tested manually, but the backend team has flagged an issue: the product API is being called twice on every page load.
This happens because when Blazor transitions from Static SSR to Interactive Server mode, it creates a new component instance that has no memory of what the static phase fetched, causing
OnInitializedAsyncto run twice. Customers on slower connections are seeing a brief loading spinner after the page appeared ready.Your job is to understand why this happens, fix the data flow across the render boundary, and diagnose the hydration issues that come with mixing render modes.
In this lab you will work through the following:
- Understanding the four Blazor render modes and how the component lifecycle behaves differently in each.
- Observing the hydration gap by adding logging and watching duplicate API calls appear in the terminal.
- Persisting data across render phases to eliminate the redundant API call.
- Passing persisted data through the component tree without reintroducing independent fetching.
- Diagnosing and fixing common hydration issues including state key mismatches, render flicker, and parameter desync after navigation.
By the end of this lab you will understand how Blazor's render pipeline moves data across phase boundaries, why the hydration gap causes redundant API calls, and how to prevent and diagnose the issues that come with mixing render modes.
Project Structure
Take a moment to familiarize yourself with the project before making any changes.
GlobomanticsStore/ Components/ Pages/ ProductCatalog.razor <- Product catalog page ProductDetail.razor <- Product detail page Shared/ ProductCard.razor <- Reusable product card component Models/ Product.cs <- Product data record CartItem.cs <- Cart item model Services/ IProductService.cs <- Service interface ProductService.cs <- Simulated product API Program.cs <- Application startup and DI registrationBefore You Begin
Open each file and read through it before making any changes. This gives you a mental map of the project before you start coding. | File | Description | |---|---| |
/ProductCatalog.razor| The main catalog page that fetches and displays all products.
This is the file you will work in most throughout the lab. | |/ProductDetail.razor| The product detail page that displays a single product
based on theIdroute parameter. | |/ProductCard.razor| A reusable presentation component that renders a single product card.
It is already fully built and ready to use. | |/Product.cs| Defines the product data structure as an immutable C# record.
All product data in the application flows through this type. | |/CartItem.cs| Represents a single line item in a shopping cart.
Used to track quantity and calculate line totals. | |/IProductService.cs| The service contract that defines how product data is retrieved.
Components depend on this interface, not the concrete implementation. | |/ProductService.cs| The simulated product API. It usesTask.Delayto mimic a slow external data source.
The delay is intentional and should not be removed. | |Program.cs| The application entry point where services are registered
and the middleware pipeline is configured. |
In the next step, you will learn how the four Blazor render modes work, register the product service with the DI container, and add the render mode directive that makes the catalog page interactive.
Note: Each step contains tasks clearly marked with comments like
// Task 2.1info>If you get stuck on a task, you can view the solution in the
solutionfolder in your Filetree, or click the Task Solution link at the bottom of each task after you've attempted it. -
Challenge
Understanding Render Modes and Data Boundaries
Blazor supports four render modes. The one you choose determines where your component runs and how many times
OnInitializedAsyncis called per page load.Choosing the wrong mode, or not choosing one at all, is the root cause of most hydration problems.
Concept: The Four Render Modes
| Mode | Where it runs |
OnInitializedAsync
calls per navigation | |---|---|---| | Static SSR | Server | Once | | Interactive Server
prerender off | Server | Once | | Interactive Server
prerender on | Server twice | Twice | | Interactive WebAssembly
prerender on | Server then browser | Twice |Static SSR renders the component to HTML on the server and sends it to the browser. Once the HTML is delivered the connection closes. There is no SignalR, no interactivity, and no further lifecycle calls. This is the default when no
@rendermodedirective is present.Interactive Server keeps a persistent SignalR connection open between the browser and the server. Every interaction is processed on the server and the result is sent back as a DOM diff. When prerendering is enabled, Blazor runs the component once statically to produce fast initial HTML, then runs it again when the interactive connection is established. This second run is what causes the double fetch in the Globomantics Store.
Interactive WebAssembly runs the component entirely in the browser inside a .NET WebAssembly runtime. With prerendering enabled, the first pass still runs on the server.
Auto starts as Interactive Server while the WebAssembly runtime downloads, then switches to WebAssembly on subsequent visits.
The catalog page currently has no
@rendermodedirective, which means it runs as Static SSR. Products render but the page has no interactivity.In this step you will add
@rendermode @(new InteractiveServerRenderMode(prerender: true))to make the page interactive.Using
new InteractiveServerRenderMode(prerender: true)makes the prerender flag explicit in the code. This is intentional. It makes it clear that prerendering is active and sets up the double-fetch behavior you will observe in next Step.Why
AddScoped? A Scoped service lives for the duration of one HTTP request in Static SSR mode, or for the duration of one SignalR circuit in Interactive Server mode. This is the correct lifetime for a service that fetches data per user session.With the service registered, the application can now inject
IProductServiceinto any component. The next task makes the catalog and detail pages interactive by adding render mode directives. > Why different directives on each page?ProductCataloguses the explicit formnew InteractiveServerRenderMode(prerender: true)so the prerender flag is visible in the code.ProductDetailuses the shorthandInteractiveServerwhich also prerenders by default.Both produce the same behavior — the explicit form is used on the catalog page intentionally so the prerender setting is visible when you observe the double fetch in next Step. In this step, you learned how Blazor's four render modes differ in where they run and how many times
OnInitializedAsyncis called. You registeredIProductServicewith the DI container and applied render mode directives to both pages. The explicitprerender: trueflag on the catalog page meansOnInitializedAsyncwill now run twice per page load.In the next step, you will make that double call visible by adding logging to
ProductServiceand watching two log entries appear in the terminal for a single page navigation. -
Challenge
Observing the Hydration Problem
This step makes the double-fetch problem visible. You will add structured logging to
ProductServiceand watch the terminal to confirm thatGetAllAsyncis called twice on a single page load.
The Hydration Gap
When
InteractiveServerRenderMode(prerender: true)is active, Blazor processes your component twice per browser request:Pass 1 — Static Prerender
The server runs
OnInitializedAsync, callsGetAllAsynconProductService, and renders the result to HTML. The HTML is sent to the browser immediately.Pass 2 — Interactive Hydration
Blazor's JavaScript boots in the browser. It opens a SignalR connection to the server. The server creates a new instance of your component and runs
OnInitializedAsyncagain. This new instance has no memory of what Pass 1 fetched.Browser navigates to /catalog | +-- Pass 1: Static prerender (server) | OnInitializedAsync runs | GetAllAsync called <-- CALL 1 | HTML sent to browser | +-- Pass 2: Interactive hydration (new server instance) OnInitializedAsync runs again GetAllAsync called <-- CALL 2This is the hydration gap. The data produced during Pass 1 is not carried across to Pass 2.
ProductServicesimulates a slow external API usingTask.Delay. It is the only place in the application where data is fetched. Adding log statements here gives you complete visibility into every API call the application makes, including the duplicate calls caused by the hydration gap.You will use the
_loggerto log messages. WithGetAllAsyncnow logged, the next task adds the same visibility toGetByIdAsyncso all API calls are traceable.Keep this logging in place. You will use it in next steps to verify your fix reduces this to a single call per page load. In this step, you observed the hydration gap in action. Two
GetAllAsynclog entries per page load confirm that the static prerender and the interactive hydration each create a fresh component instance with no shared memory. The C# objects from Pass 1 do not survive to Pass 2.In the next step, you will fix this problem using
PersistentComponentState. The server will embed the product list in the HTML payload during Pass 1 so Pass 2 can restore it without calling the API again. -
Challenge
Persisting State Across Render Phases
In this step you will learn how
PersistentComponentStatecarries data across the render boundary and implement the restore logic to eliminate the redundant API call.
Concept: PersistentComponentState
PersistentComponentStateis a Blazor service that carries data across the boundary between the static prerender phase and the interactive hydration phase. It works as a two-sided handshake:Server side — during static prerender: You register a callback with
RegisterOnPersisting. Blazor calls this callback just before flushing HTML to the browser. Inside the callback you callPersistAsJsonto serialize your data into a hidden JSON element embedded in the HTML output.Client side — during interactive hydration: You call
TryTakeFromJsonto look for the payload. If found, restore the data and skip the API call. If not found, fall back to the API normally.Pass 1: Static prerender RegisterOnPersisting registers the callback GetAllAsync called --> _products loaded PersistAsJson("products", _products) --> embedded in HTML Pass 2: Interactive hydration (new instance) TryTakeFromJson("products", out var restored) --> Found! _products = restored GetAllAsync is NOT calledThe State Key
The string
"products"linksPersistAsJsonon the server toTryTakeFromJsonon the client. Both sides must use exactly the same string.--- Review the current implementation.
Open
Components/Pages/ProductCatalog.razorand read through the code before starting your task. The server side of the handshake is already written. Understanding what each piece does will make your task much clearer.@inject PersistentComponentState AppState— injects the service that manages the data payload between the two render passes@implements IDisposable— ensures the callback registration is released when the component is removed from the pageAppState.RegisterOnPersisting(PersistDataAsync)— tells Blazor to callPersistDataAsyncjust before flushing HTML to the browser at the end of Pass 1PersistDataAsync— serializes the product list into the HTML payload usingPersistAsJson
Now you will complete the
PersistDataAsyncfunction to store theproductsasJson. With the product list now embedded in the HTML during Pass 1, the next task implements the restore logic that reads it back during Pass 2. With the restore logic in place, run the application and confirm the double fetch is gone.In this step, you implemented the
PersistentComponentStatehandshake. The server embeds the product list during Pass 1 usingPersistAsJson. Pass 2 restores it usingTryTakeFromJsonand skips the API call entirely. The single log entry per page load confirms the fix is working.In the next step, you will learn why this persistence pattern must be respected throughout the component tree. A child component that fetches data independently can silently bypass everything you just built and reintroduce the redundant API calls.
-
Challenge
Passing Persisted Data to Child Components
Fixing the double fetch at the parent level is not enough on its own. If child components independently fetch the same data, the redundant API calls return through a different path. This step teaches you the correct way to pass persisted data through the component tree and why the pattern matters.
Concept: Parent-Child Data Flow
When Blazor hydrates an interactive page, the parent component runs first. It restores the product list from the persisted payload and renders its children, passing data down as
[Parameter]values. The children render with the data they receive. They do not need to know where it came from or how it was fetched.The responsibility boundary is clear:
- Components that own data fetch it, persist it, and pass it down to their children
- Components that display data receive it as a
[Parameter]and render it — nothing more
ProductCatalogowns the product list.ProductCarddisplays one product. IfProductCardinjectedIProductServiceand calledGetAllAsyncinOnInitializedAsync, it would create its own API calls during both Pass 1 and Pass 2, completely independent of the parent's persisted state. With five products on the page that would be eleven calls per navigation — one from the parent plus ten from the five cards (two each). All the work from Step 4 would be silently undone.
Before You Begin
Open
Components/Shared/ProductCard.razorand review it before starting your task. The component accepts aProductas an[EditorRequired][Parameter]and renders the product details. It has no@inject IProductServiceand noOnInitializedAsync. Your task is to use it inProductCatalog.razor.--- In this step, you ensured that child components rely on parent-provided data instead of fetching their own. This preserves the single API call and prevents redundant requests.
Next, you will diagnose common hydration issues such as state key mismatches, render flicker, and parameter desynchronization.
-
Challenge
Diagnosing Common Hydration Issues
Even with
PersistentComponentStatecorrectly implemented, three categories of issues commonly appear in production Blazor applications. Each one is subtle, hard to spot without the right tools, and easy to fix once you understand why it happens.
Concept: Three Common Hydration Issues
Issue 1 — Silent Key Mismatch
PersistentComponentStatelinksPersistAsJsonandTryTakeFromJsonusing a plain string key. If the two strings do not match exactly,TryTakeFromJsonreturnsfalsesilently. No exception is thrown. No warning appears. The component falls back to the API call and the double fetch returns as if Step 4 never happened. Adding aLogWarningto the failure branch makes this detectable immediately during development.Issue 2 — Render Flicker from Missing Loading State
Without an explicit loading flag, the component can briefly show a loading spinner during the interactive hydration pass even when data was already restored from the persisted payload. Open
ProductCatalog.razorand review the_isLoadingflag and the skeleton markup in the@if (_isLoading)branch before starting your tasks.Issue 3 — Parameter Desync After Navigation
When a user navigates from
/product/1to/product/3, Blazor reuses the existingProductDetailcomponent instance. TheIdparameter changes butOnInitializedAsyncdoes not run again. The page continues showing the wrong product. The fix isOnParametersSetAsyncwith an integer change guard — unlikeOnInitializedAsync, it is called every time any parameter changes.
A key mismatch is invisible without diagnostic logging. The
LogWarningcall in the failure branch gives you an immediate signal in the terminal wheneverTryTakeFromJsonfails to find the payload, so you can catch this mistake before it reaches production. With key mismatch detection in place, you’ve ensured that persisted state is restored correctly during hydration.However, hydration issues are not limited to data restoration. Problems can also occur when component parameters change after the initial render.
In the next task, you will fix a parameter desync issue in
ProductDetail.razor, where the component does not react correctly when navigating between products.
When a user navigates between two routes that share the same component, Blazor reuses the existing component instance instead of creating a new one. Route parameters change but OnInitializedAsync does not run again — it only runs once per component lifetime. The page continues showing the previous data even though the URL has changed. This is called parameter desync. In this step, you diagnosed and fixed three real-world hydration issues.
A
LogWarningin theTryTakeFromJsonfailure branch makes silent key mismatches immediately visible during development.The
_isLoadingflag with skeleton markup prevents render flicker. ReplacingOnInitializedAsyncwithOnParametersSetAsyncand a change guard ensuresProductDetailalways shows the correct product after navigation.In the next step, you will verify the completed application against a final checklist and review the reusable patterns from the lab.
-
Challenge
Conclusion and Next Steps
Congratulations on completing this lab!
You started with a real-world problem in the Globomantics Store where mixing Static SSR and Interactive Server render modes caused duplicate API calls and inconsistent UI behavior. Step by step, you diagnosed the root cause and implemented a clean, production-ready solution.
In this lab, you:
-
Traced how data flows from server-side rendering to client-side interactivity
-
Identified the hydration gap, where component state is lost between render phases
-
Observed duplicate API calls by adding logging and understanding lifecycle behavior
-
Implemented persisted state to carry data across render boundaries
-
Ensured child components receive data without triggering redundant fetches
-
Diagnosed and fixed real-world issues such as:
- State key mismatches
- UI flicker during hydration
- Parameter desynchronization after navigation
By the end, you now have a repeatable pattern for managing data across render modes, ensuring both performance and consistency in Blazor applications .
Final Verification Checklist
Run
dotnet runfrom thestep6/GlobomanticsStorefolder and verify each item.| # | What to verify | How to check | |---|---|---| | 1 | Only one
GetAllAsynclog entry per/catalogpage load | Watch the terminal | | 2 |Restored 5 products from persisted stateappears in the terminal | Watch for theLogInformationmessage | | 3 | Product cards render with name, category, price, and stock badge | Open/catalogin the browser | | 4 | Skeleton placeholders appear briefly before products render | Reload/catalogand observe | | 5 | Navigating from/product/1to/product/3shows the correct product | Change the URL manually | | 6 | Mistyping the state key triggersLogWarningin the terminal | Change"products"to"productss"inPersistAsJson, reload, observe, revert |
Next Steps
You can continue building on what you’ve learned in this lab by exploring the following areas:
Change prerender settings
- Disable prerender on ProductDetail by changing the directive to
@rendermode @(new InteractiveServerRenderMode(prerender: false)) - Observe the logs for function
GetByIdAsync
Apply the Pattern to Additional Pages
- Extend persisted state handling to other parts of the application
- Ensure consistent data flow across multiple pages
Explore Interactive WebAssembly Mode
- Try switching components to Interactive WebAssembly
- Compare how hydration and data flow behave across render modes
Introduce Service-Level Caching
- Add in-memory caching in
ProductService - Reduce repeated data access beyond hydration scenarios
Work with Real APIs
- Replace the simulated service with a real backend API
- Handle latency, failures, and retries
Improve Observability
- Add structured logging across components
- Track render phases and data flow more effectively
Explore Advanced Hydration Scenarios
- Experiment with partial updates and navigation flows
- Investigate edge cases when mixing render modes
Build Reusable Patterns
- Create helper utilities for persisted state handling
- Standardize patterns that can be reused across projects
This foundation will help you build Blazor applications that are not just functional, but predictable, efficient, and production-ready.
-
About the author
Real skill practice before real-world application
Hands-on Labs are real environments created by industry experts to help you learn. These environments help you gain knowledge and experience, practice without compromising your system, test without risk, destroy without fear, and let you learn from your mistakes. Hands-on Labs: practice your skills before delivering in the real world.
Learn by doing
Engage hands-on with the tools and technologies you’re learning. You pick the skill, we provide the credentials and environment.
Follow your guide
All labs have detailed instructions and objectives, guiding you through the learning process and ensuring you understand every step.
Turn time into mastery
On average, you retain 75% more of your learning if you take time to practice. Hands-on labs set you up for success to make those skills stick.