Inconsistent persistence behavior in Blazor

iKingNinja 140 Reputation points
2025-11-26T14:16:53.35+00:00

I have an SSR MainLayout.razor and a couple other InteractiveServer (SI) components. I need to display messages across tabs and to identify each tab i'm cascading a tabId value from the layout.

Since this crosses render mode boundaries, on subsequent renders of child SI components, the cascaded value is null. I tried using persistence, however I get inconsistent behavior.

MainLayout.razor

<CascadingValue Value="@Guid.NewGuid().ToString()" Name="tabId">
    <CascadingValue Value="userClaims">        
        <GlobalMessagesArea />
        @Body
    </CascadingValue>
</CascadingValue>

GlobalMessagesArea.razor

@rendermode InteractiveServer

@code {
    [CascadingParameter(Name = "tabId")]
    public required string CascadedTabId { get; set; }

    [PersistentState]
    public required string TabId { get; set; }

    protected override async Task OnParametersSetAsync()
    {
        TabId ??= CascadedTabId;
    }
}

So far so good. However problems start with Configuration.razor where TabId is not persisted unless I hard-refresh the page or navigate to it directly (without enhanced navigation). How do I fix this?

@page "/admin/configure"
@attribute [Authorize(Roles = RoleClaimConstants.Manager)]

@rendermode InteractiveServer
   
@code {
    [CascadingParameter(Name = "tabId")]
    public required string CascadedTabId { get; set; }

    [PersistentState]
    public required string TabId { get; set; }

    protected override async Task OnInitializedAsync()
    {
        TabId ??= CascadedTabId;
    }
}
Developer technologies | .NET | Blazor
0 comments No comments
{count} votes

1 answer

Sort by: Most helpful
  1. Danny Nguyen (WICLOUD CORPORATION) 5,065 Reputation points Microsoft External Staff Moderator
    2025-11-27T10:34:00.1466667+00:00

    Hi,

    I think the problem is when the Configuration page can actually get at its persisted state compared to when it runs this line:

    TabId ??= CascadedTabId;
    

    In my set up of your code, CascadedTabId seemed to get set in a split second and then return to null shortly after.

    • On full refresh of /admin/configure
      • The interactive Configuration component starts up in a context where [PersistentState] can restore TabId.
      • So TabId is already set from persistence when OnInitializedAsync runs, and TabId ??= CascadedTabId; effectively does nothing.
      • That’s why you see the same TabId as GlobalMessagesArea after a hard refresh.
    • On enhanced navigation to /admin/configure
      • The interactive page instance is created in a different pipeline: at that point, the persisted state for this page is not available (or not wired up yet).
      • CascadedTabId is also not usable in this phase for the interactive page.
      • So when OnInitializedAsync runs:
      • TabId is still unset (persistence hasn’t provided anything),
      • CascadedTabId is effectively not giving you a usable value,
      • Result: TabId never gets a value at all on that navigation.

    So, in short: Configuration.razor initializes [PersistentState] in OnInitializedAsync on an InteractiveServer page, but under enhanced navigation the interactive instance can’t see its persisted TabId (and doesn’t get a usable cascaded tabId either) at that point. On full refresh the persisted value is available in time, so TabId matches GlobalMessagesArea; on enhanced navigation it isn’t, so TabId ends up empty.

    You need to tackle handling CascadedTabId differently


Your answer

Answers can be marked as 'Accepted' by the question author and 'Recommended' by moderators, which helps users know the answer solved the author's problem.