Search code examples
asp.net-corerazorblazor

How to make a razor component re-render from another razor component


I can't seem to make a razor component re-render from actions taken from another component. For instance:

say we have a component base 'ComponentVariables' where a variable 'buttonClickCount' is defined to keep track of how many times a button is clicked, as well as the method to increment the variable.

ComponentVariables.cs

sing Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Hosting;

namespace WebApplication2.Components
{
    public class ComponentVariables : ComponentBase
    {
        [Parameter]
        public int buttonClickCount { get; set; }

        public ComponentVariables() { }

        protected override void OnInitialized()
        {
            buttonClickCount = 0;
        }

        public void clickCounter()
        {
            buttonClickCount++;
            StateHasChanged();
        }
    }
}

Then its inherited by all components in _Imports.razor with @inherits ComponentVariables

The home component has the following code

Home.razor

<div class="row">
    <strong>@buttonClickCount</strong>
    <Clicker/>
</div>

and the button is created in the Clicker.razor component. along with an onclick that references the method in the component base. and a div containing the count (also from the component base).

Clicker.razor

<div>
    <button @onclick="clickCounter">Click Me</button>
</div>

<div>@buttonClickCount</div>

@code
{
}

As it is right now, both home.razor and clicker.razor are using the same variable in ComponentVariables.cs to display a value. however, when i click, only the Clicker component gets rerendered. i.e. home always displays 0 while clicker increases as you click.

Any help with this would be very appreciated.


Solution

  • You used inheritance. So, it might seem that you manipulate the same variable because they have the name, but in fact, they are all unique instances with unique values.

    If you want to exchange a value between different components, there are three ways to do it. This blog post describe them. Based on your description, I'd recommend an AppState approach. I've written an answer here.

    I've copied the relevant parts for my previous answer and add some new thoughts.

    Let's start by defining the AppState. This class uses the .NET events or delegates to raise a notification when a value has changed.

    public class AppState
    {
        private Int32 _clickCounter = 0;
    
        public Int32 ClickCounter
        {
            get => _clickCounter;
            set
            {
                _clickCounter = value;
                AppStateChanged?.Invoke(nameof(ClickCounter), this);
            }
        }
    
        public event AppStateChangedHandler AppStateChanged;
    }
    

    The handler public delegate void AppStateChangedHandler(String propertyName, AppState state);. The change handler includes the object itself and the propertyName, that has changed. This can be used by components subscribing to the event to filter if the change is relevant.

    The AppState is added to the dependency injection system. For Blazor WASM, it should be a singleton dependency. For Blazor Server, though, it should be a scoped one.

    public static async Task Main(string[] args)
    {
        var builder = WebAssemblyHostBuilder.CreateDefault(args);
        builder.RootComponents.Add<App>("#app");
    
        ...
        builder.Services.AddSingleton(new AppState());
        ... 
       
        await builder.Build().RunAsync();
    }
    

    The Counter component from the default template should be an example of using the AppState to propagate a change. This component is changed slightly.

    @page "/Counter"
    @inject AppState appState
    
    <h1>Counter</h1>
    
    <p>Current count: @appState.ClickCounter</p>
    
    <button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
    
    @code {
    
        private void IncrementCount()
        {
            appState.ClickCounter++;
        }
    }
    

    Instead of incrementing (and reading) a local variable, the corresponding property from the AppState is used.

    To simplify the subscription to the change event and reduce the boilerplate in each component, we create an abstract base called AppStateAwareComponentBase. This should be a normal .cs file.

    public abstract class AppStateAwareComponentBase : ComponentBase, IDisposable
    {
        [Inject]
        protected AppState AppState { get; private set; }
    
        protected override void OnInitialized()
        {
            base.OnInitialized();
    
            AppState.AppStateChanged += AppStateChanged;
        }
    
        protected abstract Boolean HandleAppStateChanged(String propertyName, AppState state);
    
        private async void AppStateChanged(String propertyName, AppState state)
        {
            Boolean changesOccured = HandleAppStateChanged(propertyName, state);
            if (changesOccured == true)
            {
                await InvokeAsync(StateHasChanged);
            }
        }
    
        public void Dispose()
        {
            AppState.AppStateChanged -= AppStateChanged;
        }
    }
    

    This class is subscribed to change event but doesn't know how to handle it exactly. Hence, it has an abstract method called HandleAppStateChanged that need to be implemented in the concrete classes later. This class determines if a layout change is a subsequent result. The child calls returns true if a layout update is needed. The parent class (AppStateAwareComponentBase) initiate an layout update via invoking InvokeAsync(StateHasChanged).

    If The AppState is injected via [Inject] and the implementation of IDisposable guarantee that the subscription to the delegate is removed when the component is removed from the tree.

    I've created a CounterHeader component, as a class inherits from AppStateAwareComponentBase

    @inherits AppStateAwareComponentBase
    
    <span>Current App Global Counter: @_counter </span>
    
    @code{
    
       private Int32 _counter = 0;
    
       protected override Boolean HandleAppStateChanged(String propertyName, AppState state)
       {
           if(propertyName != nameof(AppState.ClickCounter)) {
               return false;
           }
           
           _counter = state.ClickCounter;
           return true;
       }
    }
    

    Here is a link to the corresponding repository.

    Additional Thoughts

    This is a fundamental version of an "AppState". However, there are frameworks like Fluxor for all these state transitions, or you could use a more decoupled way like a message bus for components, called ComponentBus.

    There can be multiple "states" per application, like ShoopingCardState, UserState etc., to split it into different smaller parts.