Featured resource
2026 Tech Forecast
2026 Tech Forecast

1,500+ tech insiders, business leaders, and Pluralsight Authors share their predictions on what’s shifting fastest and how to stay ahead.

Download the forecast
  • Lab
    • Libraries: If you want this lab, consider one of these libraries.
    • Core Tech
Labs

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 platform
Lab Info
Level
Beginner
Last updated
Apr 30, 2026
Duration
45m

Contact sales

By clicking submit, you agree to our Privacy Policy and Terms of Use, and consent to receive marketing emails from Pluralsight.
Table of Contents
  1. 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 OnInitializedAsync to 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 registration
    

    Before 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 the Id route 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 uses Task.Delay to 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.1

    info>If you get stuck on a task, you can view the solution in the solution folder in your Filetree, or click the Task Solution link at the bottom of each task after you've attempted it.

  2. 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 OnInitializedAsync is 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 @rendermode directive 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 @rendermode directive, 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 IProductService into any component. The next task makes the catalog and detail pages interactive by adding render mode directives. > Why different directives on each page? ProductCatalog uses the explicit form new InteractiveServerRenderMode(prerender: true) so the prerender flag is visible in the code. ProductDetail uses the shorthand InteractiveServer which 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 OnInitializedAsync is called. You registered IProductService with the DI container and applied render mode directives to both pages. The explicit prerender: true flag on the catalog page means OnInitializedAsync will now run twice per page load.

    In the next step, you will make that double call visible by adding logging to ProductService and watching two log entries appear in the terminal for a single page navigation.

  3. Challenge

    Observing the Hydration Problem

    This step makes the double-fetch problem visible. You will add structured logging to ProductService and watch the terminal to confirm that GetAllAsync is 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, calls GetAllAsync on ProductService, 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 OnInitializedAsync again. 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 2
    

    This is the hydration gap. The data produced during Pass 1 is not carried across to Pass 2.


    ProductService simulates a slow external API using Task.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 _logger to log messages. With GetAllAsync now logged, the next task adds the same visibility to GetByIdAsync so 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 GetAllAsync log 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.

  4. Challenge

    Persisting State Across Render Phases

    In this step you will learn how PersistentComponentState carries data across the render boundary and implement the restore logic to eliminate the redundant API call.


    Concept: PersistentComponentState

    PersistentComponentState is 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 call PersistAsJson to serialize your data into a hidden JSON element embedded in the HTML output.

    Client side — during interactive hydration: You call TryTakeFromJson to 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 called
    

    The State Key

    The string "products" links PersistAsJson on the server to TryTakeFromJson on the client. Both sides must use exactly the same string.

    --- Review the current implementation.

    Open Components/Pages/ProductCatalog.razor and 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 page
    • AppState.RegisterOnPersisting(PersistDataAsync) — tells Blazor to call PersistDataAsync just before flushing HTML to the browser at the end of Pass 1
    • PersistDataAsync — serializes the product list into the HTML payload using PersistAsJson

    Now you will complete the PersistDataAsync function to store the products as Json. 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 PersistentComponentState handshake. The server embeds the product list during Pass 1 using PersistAsJson. Pass 2 restores it using TryTakeFromJson and 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.

  5. 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

    ProductCatalog owns the product list. ProductCard displays one product. If ProductCard injected IProductService and called GetAllAsync in OnInitializedAsync, 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.razor and review it before starting your task. The component accepts a Product as an [EditorRequired] [Parameter] and renders the product details. It has no @inject IProductService and no OnInitializedAsync. Your task is to use it in ProductCatalog.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.

  6. Challenge

    Diagnosing Common Hydration Issues

    Even with PersistentComponentState correctly 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

    PersistentComponentState links PersistAsJson and TryTakeFromJson using a plain string key. If the two strings do not match exactly, TryTakeFromJson returns false silently. 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 a LogWarning to 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.razor and review the _isLoading flag 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/1 to /product/3, Blazor reuses the existing ProductDetail component instance. The Id parameter changes but OnInitializedAsync does not run again. The page continues showing the wrong product. The fix is OnParametersSetAsync with an integer change guard — unlike OnInitializedAsync, it is called every time any parameter changes.


    A key mismatch is invisible without diagnostic logging. The LogWarning call in the failure branch gives you an immediate signal in the terminal whenever TryTakeFromJson fails 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 LogWarning in the TryTakeFromJson failure branch makes silent key mismatches immediately visible during development.

    The _isLoading flag with skeleton markup prevents render flicker. Replacing OnInitializedAsync with OnParametersSetAsync and a change guard ensures ProductDetail always 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.

  7. 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 run from the step6/GlobomanticsStore folder and verify each item.

    | # | What to verify | How to check | |---|---|---| | 1 | Only one GetAllAsync log entry per /catalog page load | Watch the terminal | | 2 | Restored 5 products from persisted state appears in the terminal | Watch for the LogInformation message | | 3 | Product cards render with name, category, price, and stock badge | Open /catalog in the browser | | 4 | Skeleton placeholders appear briefly before products render | Reload /catalog and observe | | 5 | Navigating from /product/1 to /product/3 shows the correct product | Change the URL manually | | 6 | Mistyping the state key triggers LogWarning in the terminal | Change "products" to "productss" in PersistAsJson, 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

Amar Sonwani is a software architect with more than twelve years of experience. He has worked extensively in the financial industry and has expertise in building scalable applications.

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.

Get started with Pluralsight