dotnet / aspnetcore

ASP.NET Core is a cross-platform .NET framework for building modern cloud-based web applications on Windows, Mac, or Linux.
https://asp.net
MIT License
35.1k stars 9.91k forks source link

Declarative model to serialize state on prerendering with Blazor and restore it on the client #26794

Open Eirenarch opened 3 years ago

Eirenarch commented 3 years ago

Consider the following scenario which is quite common: A page with a grid with a pager. The grid loads data with an AJAX call and while the data is being loaded displays a loader. It is desirable that the data is displayed prerendered when using Blazor prerendering. Currently the most straight-forward implementation of this scenario works like this

To sum it up the user sees data - loader - data. In addition two calls are made to get the data. The first issue can be mitigated by additional flags checking if we're in a first load situation on the client but it complicates the code significantly and it won't solve the second issue.

For me the additional complexity introduced by this issue was enough to turn off prerendering in my Blazor app. Maybe there is an easy workaround that I am not aware of but when I bring this issue up nobody has suggested a solution yet. The docs also seem to advise that we use caching but I don't consider this good enough as it will not remove the issue with the additional calls in Blazor wasm scenarios and still results in additional complexity.

Possible Solution

One solution I can think of is to mark certain component state with attributes and have that state be serialized while prerendering maybe as JSON and dump it into a hidden element on the page. Then when Blazor starts on the client it can read that state and initialize the component with it. This will avoid the second call for data and will simplify the code in the component.

Note that this issue is present with Blazor Server as well but with Blazor Server different approaches can be used like storing the whole component in memory and passing it upon the instantiation of the SignalR connection

jotosmurf commented 3 years ago

I was also put off by having to cache and write hacky code to avoid the FOOC effect when using prerendering. Indeed, even when the user does not see a flash, Blazor wasm still does unnecessary work as the actual data is being rendered twice.

Referencing @jonhilt here because he demonstrated the "double rendering" in this scenario in a blog post two days ago: scroll down to OnInitializedAsync is called twice.

mkArtakMSFT commented 3 years ago

Thanks for contacting us. This is potentially related to #26802 @javiercn is there another issue which we can dupe against?

ghost commented 3 years ago

We've moved this issue to the Backlog milestone. This means that it is not going to be worked on for the coming release. We will reassess the backlog following the current release and consider this item at that time. To learn more about our issue management process and to have better expectation regarding different types of issues you can read our Triage Process.

Eirenarch commented 3 years ago

@mkArtakMSFT I don't know how React works and the issue doesn't explain the details but looking at the docs for React's hydrate() it seems like it is used to attach events not to restore state from the DOM

jotosmurf commented 3 years ago

The concept needed here is called SSR client side hydradation. Examples in js SSR land explained in the first paragraph of the links below:

Issue #26802 should probably be deleted.

Eirenarch commented 3 years ago

@jotosmurf the links you provided talk about attaching events to existing HTML which is fine and great but how is the state of the component restored? If I have a List<Customer> property in my component how will this list be transferred from the server to the client?

mifopen commented 3 years ago

@mkArtakMSFT @javiercn This issue duplicates https://github.com/dotnet/aspnetcore/issues/26802 in terms of what effect both authors want to achieve. Let's remove https://github.com/dotnet/aspnetcore/issues/26802 from the backlog in favor of this issue? Also, any chance to see a solution in net6? It would be awesome to have SPA framework with SSR done right ;) (as far as I know no JS framework offer this feature for now, or, at least, without some hacks)

Eirenarch commented 3 years ago

It is great that we already have a solution for this in the latest preview but the solution seems too imperative and "manual". While having this as an option is fine I hope there will be a declarative and more transparent solution. I imagine something like marking a component with an attribute like [PreservesState] and then all the state preserving is done automatically and if the user does not want some field to be preserved they mark it with [NotPreserved] (might be good to exclude members marked with [Parameter]). Maybe even consider totally skipping the initialize event if the state has been restored declaratively.

danroth27 commented 3 years ago

@javiercn Feedback from .NET 6 Preview 2 on state persistence.

ghost commented 2 years ago

We've moved this issue to the Backlog milestone. This means that it is not going to be worked on for the coming release. We will reassess the backlog following the current release and consider this item at that time. To learn more about our issue management process and to have better expectation regarding different types of issues you can read our Triage Process.

ghost commented 8 months ago

Thanks for contacting us.

We're moving this issue to the .NET 9 Planning milestone for future evaluation / consideration. We would like to keep this around to collect more feedback, which can help us with prioritizing this work. We will re-evaluate this issue, during our next planning meeting(s). If we later determine, that the issue has no community involvement, or it's very rare and low-impact issue, we will close it - so that the team can focus on more important and high impact issues. To learn more about what to expect next and how this issue will be handled you can read more about our triage process here.

javiercn commented 2 months ago

Declarative support for persisted component state

Goals

Enable a way for state to be persisted declaratively as opposed to imperatively, as to make the feature more discoverable and easier to use.

Challenges and constraints

The main challenge that we have is that we essentially emit a dictionary to the output to support persisting the state, and each key needs to be unique. The imperative API allows you to "figure this out" by yourself to a degree, but a declarative API can't easily do so in some scenarios.

Our goal is to generate a unique key for each usage of the feature. That mapping needs to be preserved when the app restarts interactively.

There are also a couple scenarios we need to account for.

Generating a key for components.

Generating a key for services

API

This will be a new [SupplyParameterFromPersistentComponentState] attribute that when applied to a property will register it for persistence.

In addition to this, there will also be a [PersistState] attribute for services that they can apply to properties to ensure that they get persisted, with an additional Mode property to specify where things need to be persisted.

Once we enable persisted component state for enhanced navs, we'll need a UpdateOnEnhancedNavigation parameter to determine if the component wants to receive updates.

Implementation details

This gets integrated as any other cascading value. When a component is rendered and contains the attributes, we register the persistence callback, when it gets removed from the dom we unregister the callback. We only register callbacks on the SSR, on wasm/server we don't.

Eirenarch commented 2 months ago

Generating a key for components.

  • We can use the type name + property name to generate a unique key for a component that can be used to retrieve any persisted state.

    • The challenge for this is to support multiple components instances, the answer for now is to not support this scenario.
    • The alternative a not supported multiple components only requires the developer to save the state at a higher level on the hierarchy and pass that state to the components.
    • We might choose to leverage the @key attribute if available and serializable as a primitive type.

Is it possible to use the approach from web forms where the key is the ids of each of the parent components back to the root? I realize that Blazor components don't have explicit ID, but one could be generated in the background