Skip to content

RenderFragments re-rendering and state #15077

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
Postlagerkarte opened this issue Oct 16, 2019 · 9 comments
Closed

RenderFragments re-rendering and state #15077

Postlagerkarte opened this issue Oct 16, 2019 · 9 comments
Assignees
Labels
area-blazor Includes: Blazor, Razor Components question

Comments

@Postlagerkarte
Copy link

Postlagerkarte commented Oct 16, 2019

I am having a question about render fragments.

My setup looks like this:

I created a component called Zone.razor which contains this code:

@code {

    protected override void OnInitialized()
    {
        ZoneService.Register(ChildContent);
    }

    [Parameter]
    public RenderFragment ChildContent { get; set; }
}

I created another component called Output.razor which renders the fragments:

@foreach(var frag in ZoneService.GetFrags())
{
    <div>
        @frag
    </div>
}

The ZoneSerive.cs is just keeping a List with the RenderFragments.

My index.razor looks like this

<Zone>
    <Counter></Counter >
</Zone>

<Zone>
    <Counter></Counter >
</Zone>
<Output></Output>

The <output> component renders the desired output , two counter components, and the components are working fine.

However, If a StateHasChanged event triggers re-rendering the state of the counter components are lost.

Is this supposed to happen?

Can you advise how to keep the state upon re-rendering?

@javiercn javiercn added the area-blazor Includes: Blazor, Razor Components label Oct 16, 2019
@mkArtakMSFT
Copy link
Contributor

Thank you for filing this issue. In order for us to investigate this issue, please provide a minimalistic repro project (ideally a GitHub repo) that illustrates the problem.

@mkArtakMSFT mkArtakMSFT added the Needs: Author Feedback The author of this issue needs to respond in order for us to continue investigating this issue. label Oct 17, 2019
@Postlagerkarte
Copy link
Author

@mkArtakMSFT : Thank you for taking the time to investigate.

Here is the github repo: https://github.com/Postlagerkarte/renderfragment

@mkArtakMSFT mkArtakMSFT added investigate and removed Needs: Author Feedback The author of this issue needs to respond in order for us to continue investigating this issue. labels Oct 18, 2019
@mkArtakMSFT mkArtakMSFT added this to the 3.1.0-preview3 milestone Oct 18, 2019
@Andrzej-W
Copy link

@Postlagerkarte
Copy link
Author

Thank you @Andrzej-W - I already played around with the key attribute before filling the issue but now I tried again and got it to work,

I was confused because if you render like this there is no way to set the key attribute in the foreach loop:

@foreach(var frag in ZoneService.GetFrags())
{
        @frag
}

Wrapping it in an extra div and it works:

@foreach(var frag in ZoneService.GetFrags())
{
    <div @key="frag">
        @frag
    </div>
}

@mkArtakMSFT : I assume that there is no way to improve the rendering internally so that there is no need to use key attribute in the first place?

@Postlagerkarte
Copy link
Author

Postlagerkarte commented Oct 25, 2019

@Andrzej-W @mkArtakMSFT

Unfortunately adding a key only keeps the state of the renderfragment if the fragments stays within one output zone.

I updated the git hub repository (https://github.com/Postlagerkarte/renderfragment) to show the problem.

The fragments are still losing their state - even though the key property is assigned.

Basically the code looks like this now:

<Output ZoneId="1"></Output>
<Output ZoneId="2"></Output>

and the Output.razor:

@foreach(var frag in ZoneService.GetFrags(ZoneId))
{
    <div @key="frag">
        @frag
    </div>
}

@javiercn
Copy link
Member

@Postlagerkarte Thanks for providing the repro.

The problem is that you are passing a RenderFragment to a component (Zone1,Zone2,N) and then passing that same through an alternative mechanism to the output component.

The problem is that RenderFragment is not a stateful UI piece of UI, but a function that represents the UI that needs to be rendered.

In your OutputComponent you are simply retrieving that function and re-running it, which re-creates the UI from scratch, hence loosing the previous state (as expected).

You can't reuse the same component instance in two separate places of the UI, the best alternative you have is to pass in the data from a model and keep the state out of the component.

Hope this helps.

@Postlagerkarte
Copy link
Author

@javiercn : Thank you for taking the time to reply.

The context of my question is a drag and drop library I created in which a users can wrap anything in a draggable component and thus can drop it to an arbitrary dropzone.

This works like this:

<Dropzone>

        <Draggable>
            <Counter></Counter>
        </Draggable>

        <Draggable>
            <Counter></Counter>
        </Draggable>

</Dropzone>

For a single dropzone the components keep their state even when re-arranged and re-rendered. ( due to the use of the key attribute)

See here: https://blazordragdrop.azurewebsites.net/Components for a demo or here for the repo https://github.com/Postlagerkarte/blazor-dragdrop

I am pointing that out because you wrote :

"retrieving that function and re-running it, which re-creates the UI from scratch, hence loosing the previous state (as expected)."

However this is not the case in the above sample - the state is only lost when I add a second dropzone.

It seems the state is lost when a different parent component id is detected by the renderer and thus getcomponentstate can not find the state?

Would greatly appreactiate if you can elaborate a bit more on the situation and on any workaround because this problem basically renders my library unusable for stateful components.

Thank you!

@Postlagerkarte
Copy link
Author

Postlagerkarte commented Oct 28, 2019

@javiercn : Sorry to post again, just want to clarify my above posting:

My main question is:

"RenderFragment is not a stateful UI piece of UI" - which implies you can't pass it around and expect the state to survive it.

But why does this work and keeps the state:

@foreach(var frag in ZoneService.GetFrags())
{
    <div @key="frag">
        @frag
    </div>
}

Where as this doesn't work and the state is lost:

@foreach(var frag in ZoneService.GetFrags(ZoneId))
{
    <div @key="frag">
        @frag
    </div>
}

@javiercn
Copy link
Member

But why does this work and keeps the state:

Because keys don't expand multiple components. The Blazor diff algorithm diffs component tree's individually. In the first case, there is a component tree that looks something like

component="component"
element=div
key=1
{...}
element=div
key=2
{...}
element=div
key=3
{...}

While in the second case I understand each fragment is enclosed within a component
component="component"
element=div
key=1
{...}
component="component"
element=div
key=2
{...}
component="component"
element=div
key=3
{...}
component="component"
element=div
key=4
{...}

In the first case, the key can be used to detect the re-ordering and keep the internal components (preserving their state), and in the second case it can't.

This behavior is by design, and something fundamental to make rendering efficient, so you need to take it into account in the design of your component.

I believe your options are to either follow the pattern in option 1 or to pass in a model to the components that hey use to render.

@ghost ghost locked as resolved and limited conversation to collaborators Dec 2, 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 question
Projects
None yet
Development

No branches or pull requests

4 participants