Home > Enterprise >  Child component is initialized before the parent
Child component is initialized before the parent

Time:12-07

I have implemented a state container so that the parent page passes the state to 2 child components, and the child components can update the state and the parent will know about these changes. This is my state container (and it's registered as a singleton and injected into the parent and child components):

public class EditStateContainer
{
    public Entity Value { get; set; }
    public event Action OnStateChange;

    public void SetValue(Entity value)
    {
        this.Value = value;
        NotifyStateChanged();
    }
    private void NotifyStateChanged() => OnStateChange?.Invoke();
}

In the parent page I'm doing:

public void Dispose()
{
    stateContainer.OnStateChange -= StateHasChanged;
}

protected override void OnInitialized()
{
    stateContainer.OnStateChange  = StateHasChanged;

    //Create entity
    var entity = new Entity()
    {
        //Set properties here...
    };

    stateContainer.SetValue(entity);
}

And one of the child components:

protected override void OnAfterRender(bool firstRender)
{
    if (!firstRender)
        return;

    entity = stateContainer.Value;
}

However, the child component's OnAfterRender is called before the parent's OnInitialized. Why is this happening and what is the correct order of events to use?

CodePudding user response:

To answer the question, the child component doesn't initialize until its parent has initialized. I created a test project and logged all the steps that occurred.

Here's the heirarchy of what I created:

  • Index
    • IndexChild1
    • IndexChild2
      • ChildsChild1

And the result:

BlazorTests.Pages.Index.OnParametersSetAsync(ParameterView: Microsoft.AspNetCore.Components.ParameterView)
blazor.webassembly.js:1 BlazorTests.Pages.Index.OnInitialized()
blazor.webassembly.js:1 BlazorTests.Pages.Index.OnInitializedAsync()
blazor.webassembly.js:1 BlazorTests.Pages.Index.OnParametersSet()
blazor.webassembly.js:1 BlazorTests.Pages.Index.OnParametersSetAsync()
blazor.webassembly.js:1 BlazorTests.Shared.IndexChild1.OnParametersSetAsync(ParameterView: Microsoft.AspNetCore.Components.ParameterView)
blazor.webassembly.js:1 BlazorTests.Shared.IndexChild1.OnInitialized()
blazor.webassembly.js:1 BlazorTests.Shared.IndexChild1.OnInitializedAsync()
blazor.webassembly.js:1 BlazorTests.Shared.IndexChild1.OnParametersSet()
blazor.webassembly.js:1 BlazorTests.Shared.IndexChild1.OnParametersSetAsync()
blazor.webassembly.js:1 BlazorTests.Shared.IndexChild2.OnParametersSetAsync(ParameterView: Microsoft.AspNetCore.Components.ParameterView)
blazor.webassembly.js:1 BlazorTests.Shared.IndexChild2.OnInitialized()
blazor.webassembly.js:1 BlazorTests.Shared.IndexChild2.OnInitializedAsync()
blazor.webassembly.js:1 BlazorTests.Shared.IndexChild2.OnParametersSet()
blazor.webassembly.js:1 BlazorTests.Shared.IndexChild2.OnParametersSetAsync()
blazor.webassembly.js:1 BlazorTests.Shared.ChildsChild1.OnParametersSetAsync(ParameterView: Microsoft.AspNetCore.Components.ParameterView)
blazor.webassembly.js:1 BlazorTests.Shared.ChildsChild1.OnInitialized()
blazor.webassembly.js:1 BlazorTests.Shared.ChildsChild1.OnInitializedAsync()
blazor.webassembly.js:1 BlazorTests.Shared.ChildsChild1.OnParametersSet()
blazor.webassembly.js:1 BlazorTests.Shared.ChildsChild1.OnParametersSetAsync()
blazor.webassembly.js:1 BlazorTests.Pages.Index.OnAfterRender(True)
blazor.webassembly.js:1 BlazorTests.Pages.Index.OnAfterRenderAsync(True)
blazor.webassembly.js:1 BlazorTests.Shared.IndexChild1.OnAfterRender(True)
blazor.webassembly.js:1 BlazorTests.Shared.IndexChild1.OnAfterRenderAsync(True)
blazor.webassembly.js:1 BlazorTests.Shared.IndexChild2.OnAfterRender(True)
blazor.webassembly.js:1 BlazorTests.Shared.IndexChild2.OnAfterRenderAsync(True)
blazor.webassembly.js:1 BlazorTests.Shared.ChildsChild1.OnAfterRender(True)
blazor.webassembly.js:1 BlazorTests.Shared.ChildsChild1.OnAfterRenderAsync(True)

When it comes to Blazor, it seems there are many ways to skin the cat. I have also tried most. I've been working with Blazor for 8 months now and this is what I have personally defined as best practice when it comes to passing data between components.

  1. Injecting a scoped object in the DI Container. Scoped objects in Blazor seem to be the same as singletons. If I'm wrong, someone please correct me. With this, I create basic state objects that apply to the entire application.
  2. State management via Fluxor. Using Fluxor has the added benefit of updating any components that are referencing the same state models. This is great for sharing data between components that don't live on the same page or are even on different parts of the layout. However, this can be abused and cause performance issues, so don't use it with large datasets!
  3. For strict parent-child relationships for components I use CascadingValues. This allows for a tighter, well-defined scope.
  4. For generic components used in many places, Component Parameters are the way to go. In this case, you should create EventCallback parameters to notify the parent when something changes.

When it comes to a parent being notified that a child component has modified the object reference, there are also many ways of of doing this as well. You can use Fluxor and the component just needs to inherit the Fluxor component, this will take into account when the state has been modified. Other options would be to use Delegates. Wether it be an EventHandler on the model, an EventCallback on the child component, or some other Func or Action.

Here's a sample object with an event handler:

public class SomeObject
{
    private string _sampleText = "Nothing has been set yet.";

    public EventHandler? SampleTextHasChanged { get; set; }

    public string SampleText
    {
        get => _sampleText;
        set
        {
            _sampleText = value;
            SampleTextHasChanged?.Invoke(this, EventArgs.Empty);
        }
    }
}

After instantiating the object, you can set your event handler to do whatever you'd like. for example, on my Index page of my sample, I wrote this:

protected override void OnInitialized()
{
    _someObject = new SomeObject();
    Console.WriteLine($"{GetType()}.OnInitialized()");
    base.OnInitialized();
}

protected override void OnAfterRender(bool firstRender)
{
    Console.WriteLine($"{GetType()}.OnAfterRender({firstRender})");
    if (firstRender)
    {
        _someObject.SampleTextHasChanged  = (sender, args) => StateHasChanged();
    }
    base.OnAfterRender(firstRender);
}

Then I set it as a cascading value:

<CascadingValue Value="_someObject">
    <IndexChild1></IndexChild1>
    <IndexChild2></IndexChild2>
</CascadingValue>

Then in ChildsChild1.razor I added:

@if (SomeObject is not null)
{
    <button @onclick="@(() => SomeObject.SampleText = "Now you've done it.")">Don't click here!</button>
}

@code {

    [CascadingParameter]
    public Index.SomeObject? SomeObject { get; set; }
}

And it goes from this:

enter image description here

to this after clicking the button:

enter image description here

But remember, this isn't neccessarily what you need, this is just an example of how a parent can know if a child has modified an object.

CodePudding user response:

Here's my minimum reproducible example based on your code. I've made a few mods and removed the stuff from OnAfterRender which I don't think you need there. I also set the EditStateContainer as scoped as I'm assuming (maybe wrongly) it's per user session.

Have a look at it and change it to reproduce your problem.

public class EditStateContainer
{
    public Entity Value { get; set; } = new Entity();

    // Changed to EventHander as they are designed for muticast operations
    public event EventHandler? OnStateChanged;

    public void SetValue(Entity value)
    {
        this.Value = value;
        NotifyStateChanged();
    }
    private void NotifyStateChanged() 
        => this.OnStateChanged?.Invoke(null, EventArgs.Empty);
}

// my minimal Entity
public class Entity
{
    public string Value { get; set; } = DateTime.Now.ToLongTimeString();
}

My component:

@inject EditStateContainer stateContainer

<div >
    <div >
        <h3>@(this.stateContainer.Value.Value)</h3>
    </div>

    <div>
        <button  @onclick=UpdateState> Update State</button>
    </div>
</div>

@code {
    protected override void OnInitialized()
        => stateContainer.OnStateChanged  = OnStateChanged;

    private Task UpdateState()
    {
        this.stateContainer.SetValue(new Entity() { Value = DateTime.Now.ToLongTimeString() });
        return Task.CompletedTask;
    }

    private void OnStateChanged(object? sender, EventArgs e)
        => this.InvokeAsync(this.StateHasChanged);

    public void Dispose()
        => stateContainer.OnStateChanged -= this.OnStateChanged;
}

And the test page:

@page "/"
@inject EditStateContainer stateContainer

<PageTitle>Index</PageTitle>

<h1>Hello, world!</h1>

Welcome to your new app.

<SurveyPrompt Title="How is Blazor working for you?" />
<Component/>
<Component />

<div >
    <h3>@(this.stateContainer.Value.Value)</h3>
</div>

<div>
    <button  @onclick=UpdateState> Update State</button>
</div>
@code {

    protected override void OnInitialized()
    {
        // move to before setting the handler - it will get rendered correctly and setting it after will cause multiple renders
        stateContainer.SetValue(new Entity() { Value = DateTime.Now.ToLongTimeString() });

        stateContainer.OnStateChanged  = OnStateChanged;
    }

    private Task UpdateState()
    {
        this.stateContainer.SetValue(new Entity() { Value = DateTime.Now.ToLongTimeString() });
        return Task.CompletedTask;
    }

    // Invoke it to ensure it runs on the UI thread.  You can never be sure which thread the caller might be on.
    private void OnStateChanged(object? sender, EventArgs e)
        => this.InvokeAsync(this.StateHasChanged);

    public void Dispose()
        => stateContainer.OnStateChanged -= this.OnStateChanged;
}
  • Related