Why isn’t sibling component updating its content?

I am developing a Blazor server app. One of the classes is Employee.cs which is injected at Startup.cs

Employee.cs

public class Employee
{
    public string Name { get; set; } = "James";
}

Startup.cs

  ...
  ...
  services.AddSingleton<Employee>();

To use this injected instance, I wrote a simple component Welcome.razor

Welcome.razor

@inject Employee Emp

<h1>Welcome [email protected]</h1>

The Welcome component is a permanent part of the UI, and is displayed at the top of the page.

I created a razor page ChangeName.razor that also displays the Name from the injected instance, and a button to alter the value. When the button is clicked, the name is changed within the page containing the button, but has no effect on the Welcome.razor even after calling StateHasChanged()

ChangeName.razor

@page "/"
@inject Employee Emp

<button @onclick="@ChangeName">Change Name</button>
<div>Employee name in page is: @Emp.Name</div>

@code {
    void ChangeName() {

        Emp.Name = "Mike";

        StateHasChanged();    // has no effect on the Welcome.razor content

    }
}

Once the button is clicked, the name in Welcome.razor stays as James but the name in ChangeName.razor page is changed to Mike. Why aren’t they synced?

Here is what I did as a work-around:

Welcome.razor

@code {
    static Welcome _welcome;

    protected override void OnInitialized()
    {
        base.OnInitialized();
        _welcome = this;        // grab a reference to the current instance 
                                // and assign it to the static instance
    }

    public Refresh() {
       _welcome.StateHasChanged();    // this can be called from any other component now
    }
}

Now I can just call Welcome.Refresh() from anywhere and the name will change. But this solution is ugly and there must be something wrong that I am doing..

What can I do to automatically trigger StateHasChanged() for any component that is displaying a shared object?

  • Your work-around shouldn’t work (because of AddTransient).

    – 

  • The Employee is transient, so for every @inject Employee Emp you have a new instance… btw you might appreciate ` <SectionOutlet` that was added in .NET8.

    – 

  • @ℍℍ I changed Transient to Scoped and Singleton but got the same result

    – 

  • @Alamakanambra I updated my question, I used Singleton and yet the same issue still happens

    – 

  • My other guess is that you crossing Interactivity boundaries. Blazor initialize its own set of services for SSR and for Blazor Server. When the Change name and Welcome are in different interactivity modes, they won’t have the same instance even if it’s a singleton. Check if the Employee is the same instance (assign random Id to it in default ctor and display this id)..

    – 

[Polite] You are making several fundamental mistakes.

  1. Read the Blazor Service Scope documentation – Transient is one instance per DI service provider request. The service needs to be Scoped to be a shared instance across the SPA session.

  2. Don’t tightly couple components by passing around references. You didn’t create the instances and aren’t in control of their lifecycles. You can end up with a reference a stale component that’s no longer part of the RenderTree and has been disposed.

  3. Use the Notification pattern – a service or cascaded object with setters and one or more change events – to communicate between distant components i.e. where you can’t use parameters and Event Callbacks.

  4. There are very few use cases where you need to call StateHasChanged. The primary one is in an Event Handler for a notification as outlined in 3. Outside that, if you’re resorting to StateHasChanged to try and update the UI, you’re in trouble. Treat the cause not the symptom. Fix the logic.

Links to other questions and answers on the Notification Pattern – Search SO for “Blazor Notification Pattern” for more.

How can I trigger/refresh my main .RAZOR page from all of its sub-components within that main .RAZOR page when an API call is complete?

Passing shared parameter to blazorcomponents

Can a Blazor element access data in a peer element?

I need to update state of blazor component when other component changed

Using the Notification Pattern

You need to implement the Notification pattern.

Yes Refresh() is not just ugly, it’s horrible. It just makes a protected method public. It’s protected for some very good reasons. Respect the design decision.

The project template for this code is Net8.0 “Blazor Web App” with either InteractiveServer or InteractiveWebAssembly and Global interactivity options selected.

public record Employee(string Name);

public class EmployeeService
{
    public Employee Employee { get; private set; } = new("Fred"); 
    public event EventHandler<Employee>? EmployeeChanged;

    public void UpdateEmployeee(Employee employee)
    {
        this.Employee = employee;
        this.EmployeeChanged?.Invoke(this, employee);
    }
}

Registered:

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddRazorComponents()
    .AddInteractiveServerComponents();

builder.Services.AddScoped<EmployeeService>();

var app = builder.Build();

Welcome:

@inject EmployeeService EmployeeService

<h1>Welcome @EmployeeService.Employee.Name</h1>

@code {
    protected override void OnInitialized()
        => this.EmployeeService.EmployeeChanged += this.OnEmployeeChanged;

    private void OnEmployeeChanged(object? sender, Employee employee)
    => this.InvokeAsync(StateHasChanged);

    public void Dispose()
        => this.EmployeeService.EmployeeChanged -= this.OnEmployeeChanged;
}

Demo Page:

@page "/"
@inject EmployeeService EmployeeService

<PageTitle>Home</PageTitle>

<Welcome />

<div>
    <button class="btn btn-primary" @onclick="this.ChangeEmployee">Change Employee</button>
</div>

@code {
    private Employee _fred = new("Fred");
    private Employee _shaun = new("Shaun");

    private void ChangeEmployee()
    {
        if (this.EmployeeService.Employee == _fred)
            this.EmployeeService.UpdateEmployeee(_shaun);
        else
            this.EmployeeService.UpdateEmployeee(_fred);
    }
}

I’ve used records [I believe in mutation control], but you can switch to classes if you prefer.

Leave a Comment