In Blazor, why does invoking NavigationManager.NavigateTo(string) sometimes result in an extra invocation of OnParametersSetAsync call with old values?
I have a page that reacts to a click with a CallbackEvent, the parent calls NavigationManager.NavigateTo to set up a new URL, which causes the Parameters of the parent to update, then the component responds to the new value, via OnParametersSetAsync. But before it does, there also was an invocation of OnParametersSetAsync with the old values--apparently from before they changed. It seems that often my second invocation finishes before the first (bad) invocation, and thus the first invocation finishes last, and I am left with a bad result.
So, is this first seemingly spurious and incorrect invocation call to OnParametersSetAsync just happening because a property may have changed when the handler of the event awaits? And how do I determine that it is a spurious call?
Here's an example that triggered it:
@page "/demo"
@page "/demo/{SelectedOrderId:int}"
@using Microsoft.Extensions.Configuration
@inject IHttpClientFactory clientFactory
@inject IToastService toastService
@inject IConfiguration Configuration
@inject NavigationManager navigationManager
<div class="mt-4 container-fluid">
<div class="row">
<div class="col-12 col-md-8 order-md-2">
Order @OrderDetail?.OrderId @OrderDetail?.ProductCode
</div>
<div class="col-12 col-md-4 order-md-1">
@foreach (var order in Orders)
{
<button @onclick="async () => await OnSelectedOrderIdChanged(order.OrderId)">@order.OrderId</button> }
</div>
</div>
</div>
@code {
[CascadingParameter]
public AppState State { get; set; } = null!;
public int? CustomerId { get; set; } = 8010;
[Parameter]
public int? SelectedOrderId { get; set; } = null;
private List<Order> Orders { get; set; } = new List<Order>();
private OrderDetail? OrderDetail { get; set; } = null;
protected override async Task OnInitializedAsync()
{
await LoadOrdersList();
}
protected override async Task OnParametersSetAsync()
{
Console.WriteLine($"params changed to order {SelectedOrderId}");
await LoadOrderDetail();
Console.WriteLine($"done loading order {SelectedOrderId}");
}
private async Task LoadOrdersList()
{
string serviceEndpoint = Configuration["MyServiceUrl"];
string url = $"{serviceEndpoint}/orders?customerId={CustomerId}";
Orders = await clientFactory.CreateClient().GetFromJsonAsync<List<Order>>(url);
}
private async Task LoadOrderDetail()
{
string serviceEndpoint = Configuration["MyServiceUrl"];
string url = $"{serviceEndpoint}/orders/{SelectedOrderId}";
OrderDetail = await clientFactory.CreateClient().GetFromJsonAsync<OrderDetail>(url);
}
private async Task OnSelectedOrderIdChanged(int? newOrderId)
{
SelectedOrderId = newOrderId;
await Task.Yield(); // could be anything
navigationManager.NavigateTo($"/demo/{newOrderId}");
}
}
When I run this and then click on one of the button for order 48026528, I get in the console:
params changed to order 48026500
params changed to order 48026528
done loading order 48026528
And the display in the page shows order 48026500! Presumably the UI update that should result happened early. So how do I determine that the invocation for 48026500 is bogus (or that it doesn't represent a real change to the parameters)?
I tried tracking the previous value and comparing it to the current value, but that doesn't help because the "new" value is processed before the old value--in other words, LoadOrderDetail() above runs twice, and they race, and often LoadOrderDetail with the new value runs first, then LoadOrderDetail with the old value completes and overwrites the new data. So how do I avoid calling LoadOrderDetail on these spurious events?
NavigationManager.NavigateTo(string) is only invoking the one time. Here is your issue (you are very close!):
[Parameter]
public int? SelectedOrderId { get; set; } = null;
. . . . .
private async Task OnSelectedOrderIdChanged(int? newOrderId)
{
// Setting a [Parameter] prop might "work", but is not intended
SelectedOrderId = newOrderId;
await Task.Yield();
// This will also update SelectedOrderId
navigationManager.NavigateTo($"/demo/{newOrderId}");
}
Note that [Parameter] properties should be set by parents or via @page route parameters, never by the component itself.
navigationManager.NavigateTo($"/demo/{newOrderId}"); will set SelectedOrderId; this is the correct way to update a page [Parameter] property! Thus, SelectedOrderId = newOrderId is not necessary. Remove the line and your code should work as expected.
Additionally, you mentioned tracking the previous value to determine when to update. This can be useful (especially when paired with ShouldRender). The easiest way to do this is to track the previous id instead of the previous value. Applied to your code:
[Parameter]
public int? SelectedOrderId { get; set; }
private int? PreviousOrderId { get; set; }
. . . . .
protected override async Task OnParametersSetAsync()
{
Console.WriteLine($"params changed to order {SelectedOrderId}");
if (PreviousOrderId != SelectedOrderId) {
// Prevent the load from ever happening if the id doesn't change.
await LoadOrderDetail();
}
PreviousOrderId = SelectedOrderId;
Console.WriteLine($"done loading order {SelectedOrderId}");
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With