Skip to content

[Blazor] Problems with Component rendering in a loop #7048

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
MartinF opened this issue Jan 27, 2019 · 2 comments
Closed

[Blazor] Problems with Component rendering in a loop #7048

MartinF opened this issue Jan 27, 2019 · 2 comments
Labels
area-blazor Includes: Blazor, Razor Components

Comments

@MartinF
Copy link

MartinF commented Jan 27, 2019

Is your feature request related to a problem? Please describe.

Related to Blazor 0.7.0 and Blazor Components.

The way rendering of Components in a loop is handled causes me some frustration and maybe it could be improved.

I am sure you have probably thought a lot about how to handle this and maybe I am going about it in the wrong way.

Component rendering and state
It is not possible to keep state in a Component when used in a loop, since it will be reused and parameters updated whenever components are being rendered, which requires state to be stored and synchronized to a long lived object (singleton, static etc.), so component state can be restored when re-rendering - sort of what have been done with AppState in the "FlightFinder" demo application, except it doesn't have any additional component state that needs to be synchronized and restored, which is what ends up causing the headache.

I have components which dynamically changes in state based on both user actions and also timers that automatically add or remove to a list or modifies the data.

Here is a simple example to set the scene:

@foreach (var msg in _list)
{
    <Test Message="@msg" />
}

@functions
{
    List<string> _list = new List<string>() {"Message1", "Message2"};
}

This will render 2 Test Components with the Message property set to "Message1" and "Message2":

Component1 = "Message1" - new instance, Init() called
Component2 = "Message2" - new instance, Init() called

If "Message1" is removed from the list and StateHasChanged() is called to re-render:

Component1 = "Message2" - update
Component2 - instance is disposed.

Init() will not be called on Component1 since it is being re-used and the parameter is updated.
Component2 will be disposed even though it contains "Message2" which still exists.
Now internal state of Component1 need to be restored with the state of Component2 as it is now displaying the content of what was previously shown in Component2.

Now lets say you render the list in reverse order because you want new items to show up at the top of the list instead of at the bottom. The initial list only contains "Message1". The following is rendered:

Component1 = "Message1" - new instance, Init() called

Now "Message2" is prepended to the list and StateHasChanged() is called to re-render the list:

Component1 = "Message2" - update
Component2 = "Message1" - new instance, Init() called (again for "Message1")

This means that in Init() you can't setup anything related to the data of the Component, since it will never be triggered for "Message2", and data can be switched around between components at any time.
So a Component is basically a template and cannot hold any state (maybe that's what got me confused in the first place as I was thinking of it in another way than just a template for rendering).

This has lead me to use ComponentModels (like a ViewModel) that holds the state and the initial data/model, but this also means that I now have to wrap all data/models with this ComponentModel and take care of storing the ComponentModel in a long lived object (static, singleton etc.), so state can be restored when re-rendered.

References
Another thing is that getting and handling the reference to a Component generated in a loop is somewhat difficult.
One way around it, is to create a property and let the setter handle it, but then you need to track components that is no longer rendered and which needs to be removed from the list with component references and disposed.
Example:

@foreach (var msg in _list)
{
    <Test Message="@msg" Ref="@Ref" />
}

@functions
{
    List<string> _list = new List<string>() {"Message1", "Message2"};
    List<Test> _refs = new List<Test>();

    protected Test Ref
    {
        get => throw new NotSupportedException("Not supported");
        set => _refs.Add(value);
    }
}

Alternatively you need to let the Component register itself with its parent(s), either using

  • CascadingValue
  • Reference to the parent that is supplied using a property parameter on the the component using some standard interface so it doesn't have a hard dependency on the parent/context.
  • Two event delegates - RefBind/OnRefBind (happens after first render since ref is not available before), and RefUnbind/OnRefUnbind (happens when disposed or if component is not rendered - but not sure how it would currently be detected).

This can of course be implemented by extending BlazorComponent, but a standardized way for handling this would be great.

Describe the solution you'd like

Component rendering and state
I'm not exactly sure how to solve the problem, and even if it should be considered one.
One way of controlling which component(s) gets instantiated, updated or disposed could be if it was possible to supply an Id/Identifier (object) to the Component which will be used to determine how it is handled:

  • If Id does not equal an Id of a previously rendered Component, a new instance of the Component is created.
  • If Id equals an Id of a previously rendered Component, the existing instance is updated with the parameters supplied.
  • Any Component previously rendered that is no longer used/updated is Disposed, which happens already.

References
It would be great to have some standardized way of handling it.
Let the Ref attribute have an overload that take a delegate/lambda that can be used to register reference and signal a change that causes it to be unregistered. Either state is supplied as a parameter in a Action<TComponent, State> or Action is used and Component has a property like "IsRendered" that can be checked to know if reference should be stored or removed.

Alternatively as mentioned previously you can bind to events like RefBind/OnRefBind and RefUnbind/OnRefUnbind on a Component, as an alternative to the "Ref" attribute.

Additional context

On one hand I understand what is going on and why it happens - it finally makes sense now.
But I wish there where some better way for handling both how components are created/updated and how references are retrieved / managed in order to prevent re-rendering and state handling, but if state should never be stored in the component then it of course doesn't matter.

At first thought maintaining state in the component would make life much easier.
But maybe it is not well thought true and storing state in the component is not desirable and will end up with negative side-effects.

@Eilon Eilon added the area-mvc Includes: MVC, Actions and Controllers, Localization, CORS, most templates label Jan 28, 2019
@Andrzej-W
Copy link

@MartinF your problem is tracked here #5455 as:

Support key to guarantee preservation of elements and component instances

@danroth27 danroth27 added the area-blazor Includes: Blazor, Razor Components label Feb 6, 2019
@mkArtakMSFT
Copy link
Contributor

Closing in favor of #8232

@mkArtakMSFT mkArtakMSFT removed area-mvc Includes: MVC, Actions and Controllers, Localization, CORS, most templates labels May 9, 2019
@ghost ghost locked as resolved and limited conversation to collaborators Dec 3, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
area-blazor Includes: Blazor, Razor Components
Projects
None yet
Development

No branches or pull requests

5 participants