Skip to content

API review for support persistent component state on enhanced navigation #62773

@javiercn

Description

@javiercn

Background and Motivation

We want to support choosing when properties annotated with PersistentComponentState should be restored as this is required for supporting persistent component state (PCS) in enhanced navigation scenarios.

Currently:

  • Prerendering and Resume work by default
    • Apps might want to restore state only during prerendering or only during resume
  • Enhanced navigation updates are off by default
    • They can override existing user state, which is not good
    • The app needs to opt-in to receive updates for individual values

The new RestoreContext and RestoreOptions types support choosing when to restore values. The RestoreContext provides three scenarios: InitialValue (when the host starts), LastSnapshot (when reconnecting), and ValueUpdate (for enhanced navigation updates). The RestoreOptions allows configuring the RestoreBehavior and whether to allow updates.

Proposed API

namespace Microsoft.AspNetCore.Components
{
    public class PersistentComponentState
    {
+        public RestoringComponentStateSubscription RegisterOnRestoring(Action callback, RestoreOptions options);
    }

+    public readonly struct RestoreOptions
+    {
+        public RestoreOptions();
+        public RestoreBehavior RestoreBehavior { get; init; }
+        public bool AllowUpdates { get; init; }
+    }

+    [Flags]
+    public enum RestoreBehavior
+    {
+        Default = 0,
+        SkipInitialValue = 1,
+        SkipLastSnapshot = 2
+    }

+    public sealed class RestoreContext
+    {
+        public static RestoreContext InitialValue { get; }
+        public static RestoreContext LastSnapshot { get; }
+        public static RestoreContext ValueUpdate { get; }
+    }

+    public readonly struct RestoringComponentStateSubscription : IDisposable
+    {
+        public RestoringComponentStateSubscription();
+        public void Dispose();
+    }

    [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
    public sealed class PersistentStateAttribute : CascadingParameterAttribute
    {
+        public RestoreBehavior RestoreBehavior { get; set; }
+        public bool AllowUpdates { get; set; }
    }
}

namespace Microsoft.AspNetCore.Components.Infrastructure
{
    public class ComponentStatePersistenceManager
    {
+        public Task RestoreStateAsync(IPersistentComponentStateStore store, RestoreContext context);
    }
}

namespace Microsoft.AspNetCore.Components.Rendering
{
    public class ComponentState : IAsyncDisposable
    {
+        protected internal Renderer Renderer { get; }
    }
}

Usage Examples

@page "/clientfetchdata/"
@using BlazorUnitedApp.Client.Data
@using Microsoft.AspNetCore.Components
@inject ClientWeatherForecastService ForecastService

<PageTitle>Weather forecast</PageTitle>

<h1>Weather forecast</h1>

<p>This component demonstrates fetching data from a service.</p>

@if (Forecasts == null)
{
    <p><em>Loading...</em></p>
}
else
{
    <table class="table">
        <thead>
            <tr>
                <th>Date</th>
                <th>Temp. (C)</th>
                <th>Temp. (F)</th>
                <th>Summary</th>
            </tr>
        </thead>
        <tbody>
            @foreach (var forecast in Forecasts)
            {
                <tr>
                    <td>@forecast.Date.ToShortDateString()</td>
                    <td>@forecast.TemperatureC</td>
                    <td>@forecast.TemperatureF</td>
                    <td>@forecast.Summary</td>
                </tr>
            }
        </tbody>
    </table>
}

@code {
    // Persist state and allow updates during enhanced navigation
    [PersistentState(AllowUpdates = true)]
    public ClientWeatherForecast[]? Forecasts { get; set; }

    public string Page { get; set; } = "";

    protected override async Task OnInitializedAsync()
    {
        Forecasts ??= await ForecastService.GetForecastAsync(DateOnly.FromDateTime(DateTime.Now));
    }
}

Additional examples showing different restore behaviors:

// Skip restoring on initial load but allow on reconnection
[PersistentState(RestoreBehavior = RestoreBehavior.SkipInitialValue)]
public string UserDraftData { get; set; }

// Skip restoring on reconnection but allow on initial load
[PersistentState(RestoreBehavior = RestoreBehavior.SkipLastSnapshot)]
public string InitialSettings { get; set; }

// Allow updates from enhanced navigation
[PersistentState(AllowUpdates = true)]
public List<TodoItem> TodoItems { get; set; }

// Programmatic restoration with custom options
protected override void OnInitialized()
{
    var options = new RestoreOptions
    {
        RestoreBehavior = RestoreBehavior.Default,
        AllowUpdates = true
    };

    State.RegisterOnRestoring(() =>
    {
        // Custom restoration logic
        LoadCustomData();
    }, options);
}

Alternative Designs

Fold enhanced navigation into RestoreStateOnPrerendering and do not have enabled/disabled ctor, and instead only provide an attribute for non-defaults:

  • public sealed class DisableRestoreStateOnResumeAttribute
  • public sealed class DisableRestoreStateOnPrerenderingAttribute
  • public sealed class RestoreStateOnPrerendering(UpdateOnEnhancedNavigation = true)

We considered making names more "agnostic" to avoid references to circuit, prerendering, enhanced navigation in the Microsoft.AspNetCore.Components assembly. Choosing more agnostic terms allows us to avoid those problematic terms and potentially simplify the design and implementation in the following ways:

  • Instead of attributes, we can have properties on PersistentState: HostStart, HostResume, AllowUpdates
  • All the types in Components.Web can live on Components
  • We can think of a day where we "resume" the host on other contexts. For example, if we also start supporting tearing down and restarting wasm apps on demand, or if we support an auto policy where we tear down the server components and restart them on wasm without reload

Risks

N/A

Metadata

Metadata

Assignees

No one assigned

    Labels

    api-approvedAPI was approved in API review, it can be implementedarea-blazorIncludes: Blazor, Razor Components

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions