'How to re-render Blazor component when a parameter changes

I have two Blazor component. First component just displays the list of students from a JSON api

<select @onchange="selectStudent"> 
  @foreach(var student in students) {
    <option value="@student.id"> @student.name /option>
  }
</select>

@code {
    var API = "https://abcd.com/students/"
    students = // variable stores JSON data of students used in the foreach loop


    // sending select event to parent
    [Parameter]
    public EventCallBack<string> OnStudentSelect { get; set; }
    public async Task SelectStudent(ChangeEventArgs e) {
        await OnStudentSelect.InvokeAsync(e.Value.ToString())
    }
}

When a user selects a student from the drop-down, I want to capture student.id and send it as a param to another component

@page "/student"

<Students OnStudentSelect="@GetStudentId"> </Students>     

<p> Displaying profile of StudentID: @studentId </p>
<Student StudentId="@StudentId"> </Student>

@code{
    private int StudentId = 1; 

    private void GetStudentId(int _id) {
        studentId = _id
    }
}

This is a snippet of my code, but it works and I can see the message change inside the <p></p> tags.

The issue I have is this:

<Student StudentId="StudentId"> </Student>

For some reason the component does not update when it receives a new StudentId

The documentation on the StateHasChanged() is not clear, but placing it inside Student does not seem to fix the issue either.



Solution 1:[1]

Until you post some more code, this is the current answer to your question:

<Student StudentId="StudentId"> </Student>

StudentId is a string, so you are passing it the literal "StudentId" as a string. No matter how many times you change the select, the value of StudentId remains "StudentId". The renderer doesn't call SetParametersAsync on Student (which calls StateHasChanged) and therefore no re-render.

In general forget using StateHasChanged to force component updates except in a couple of specific circumstances (event handlers and multiple UI updates within a single event).

If you call it to force a UI update, your code logic is almost certainly wrong.

Solution 2:[2]

After reading your post and the comments, I decided to write some components that I feel will 1. answer your question, and 2. show you a good way to write your blazor components in the future. Lets start with the first component, the StudentSelector.razor:

StudentSelector.razor

@using BlazorAnswers.Models

@*Show loading content while students are loading*@
@if(!StudentList.Any())
{
    @LoadingContent
}
else
{
    <label>Select a Student: </label>
    <select @onchange="HandleStudentChanged" class="form-select w-25">
        @foreach (var student in StudentList)
        {
            <option value="@student.Id">@student.Name</option>
        }
    </select>
}

@code {
    [Parameter] public EventCallback<StudentModel> OnStudentChanged { get; set; }

    [Parameter] public IEnumerable<StudentModel> StudentList { get; set; } = Enumerable.Empty<StudentModel>();

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

    /// <summary>
    /// This transforms the ChangeEventArgs into a StudentModel
    /// </summary>
    private async Task HandleStudentChanged(ChangeEventArgs args)
    {
        if (OnStudentChanged.HasDelegate)
        {
            if (args is not null && int.TryParse((string)args.Value, out var id))
            {
                var selectedStudent = StudentList.FirstOrDefault(a => a.Id.Equals(id));

                if (selectedStudent is not null)
                    await OnStudentChanged.InvokeAsync(selectedStudent);
            }
        }
    }
}

The next component that you should have is one to display the selected student:

StudentDisplay.razor

@using BlazorAnswers.Models

@if (SelectedStudent is not null)
{
    @StudentContent(SelectedStudent)
}

@code {
    [Parameter] public StudentModel? SelectedStudent { get; set; }

    [Parameter] public RenderFragment<StudentModel> StudentContent { get; set; } = default!;
}

Then finally everything is put together in page Student.razor which is a navigable page:

Student.razor

@page "/student"
@using BlazorAnswers.Models

<StudentSelector OnStudentChanged="HandleStudentSelected"
                 StudentList="@_students">
    <LoadingContent>
        Loading Students...
    </LoadingContent>
</StudentSelector>

@if (_selectedStudent is not null)
{
    <div class="container-lg pt-4">
        <StudentDisplay SelectedStudent="@_selectedStudent">
            <StudentContent Context="student">
                <p> Displaying profile of Id: @student.Id </p>
                <p> Displaying profile of Name: @student.Name </p>
            </StudentContent>
        </StudentDisplay>
    </div>
}

@code {
    private StudentModel _selectedStudent;
    private IEnumerable<StudentModel> _students = Enumerable.Empty<StudentModel>();

    protected override async Task OnInitializedAsync()
    {
        try
        {
            await GetStudents();
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex.Message);
        }
    }
    
    /// <summary>
    /// Method that will be invoked in the StudentSelector.razor component
    /// </summary>
    private void HandleStudentSelected(StudentModel selectedStudent)
    {
        _selectedStudent = selectedStudent;
    }
    
    /// <summary>
    /// Method for retrieving students and is intentionally delayed
    /// </summary>    
    private async Task GetStudents()
    {
        await Task.Delay(3000);

        _students = _selectableStudents;
    }

    /// <summary>
    /// Test Data to use as an example
    /// </summary>
    private static IEnumerable<StudentModel> _selectableStudents = new List<StudentModel>()
    {
        new StudentModel{ Id = 1, Name = "Joe" },
        new StudentModel{ Id = 2, Name = "Amy" },
        new StudentModel{ Id = 3, Name = "Beth" },
        new StudentModel{ Id = 4, Name = "Kevin" },
        new StudentModel{ Id = 5, Name = "Carl" },
        new StudentModel{ Id = 6, Name = "Chad" },
        new StudentModel{ Id = 7, Name = "Bryan" },
        new StudentModel{ Id = 8, Name = "Kelly" },
        new StudentModel{ Id = 9, Name = "Steve" },
        new StudentModel{ Id = 10, Name = "Doug" },
    };
}

Then here is the StudentModel class:

StudentModel.cs

public class StudentModel
{
  public int Id { get; set; }
  public string Name { get; set; } = string.Empty;
}

One other thing to consider when designing your blazor app, keep you components dumb and your pages smart. That means when injecting services try to keep them in the pages and not let them into the components themselves. This has prevented a lot of complications and allows you to build reusable components. I know someone said something about calling StateHasChanged but try to use that only when changes in your UI occur from non-human interaction.enter image description here

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 MrC aka Shaun Curtis
Solution 2