Blazor 子组件渲染和参数变化

3
我有一个组件,它接受一个int参数 - 组件使用该参数进行API调用以检索一些数据。当前这个逻辑在组件的OnParametersSetAsync()中。
此组件还有一个复杂类型参数。
当此组件由重新渲染自身的父组件使用时,即使没有更改其任何参数,也会调用此子组件的OnParametersSetAsync()。我的理解是,这是因为复杂类型参数(Blazor无法确定是否已更改,因此假定已更改)。
这将导致API调用不必要地重新触发(实际int参数未更改)。
OnParametersSetAsync()中这样做是否合适?如果不是,我应该如何更改我的组件以与Blazor框架一起工作? 父组件 调用ChangeName()触发父组件的重新渲染
<div>
    <EditForm Model="favoriteNumber">
        <InputSelect @bind-Value="favoriteNumber">
            <option value="0">zero</option>
            <option value="1">one</option>
            <option value="2">two</option>
            <option value="3">three</option>
        </InputSelect>
    </EditForm>
    
    @* This is the child-component in question *@
    <TestComponent FavoriteNumber="favoriteNumber" FavoriteBook="favoriteBook" />
    <br />

    <EditForm Model="person">
        First Name:
        <InputText @bind-Value="person.FirstName" />
        <br />
        Last Name:
        <InputText @bind-Value="person.LastName" />
    </EditForm>

    <button @onclick="ChangeName">Change Name</button>
</div>

@code {
    private int favoriteNumber = 0;
    private Book favoriteBook = new();
    private Person person = new() { FirstName = "Joe", LastName = "Smith" };

    private void ChangeName()
    {
        person.FirstName = person.FirstName == "Susan" ? "Joe" : "Susan";
        person.LastName = person.LastName == "Smith" ? "Williams" : "Smith";
    }
}

子组件

<div>@infoAboutFavoriteNumber</div>

@code {
    [Parameter]
    public int FavoriteNumber { get; set; }

    [Parameter]
    public Book FavoriteBook { get; set; }

    private string infoAboutFavoriteNumber = "";

    protected override async Task OnParametersSetAsync()
    {
        infoAboutFavoriteNumber = await ApiService.GetAsync<string>(id: FavoriteNumber.ToString());
    }
}

@Chriss:这些答案中有任何一个回答了你的问题吗?你有什么遗漏的吗? - Liero
@Liero - 我周末有点忙。 :) - Chris
5个回答

4

组件使用它来进行API调用以检索某些数据。

您的子组件不应执行任何API调用。父组件应该管理父级自身和其子级的状态,并将数据向下传递。如果情况变得复杂,那么您将需要实现一个处理状态的服务。 @Peter Morris肯定会建议您使用Blazor使用Fluxor进行高级状态管理

不确定为什么要使用两个EditForm组件,实际上不应该使用任何一个。请注意,组件非常昂贵,并且会使您的代码变慢。因此,请明智地使用它。

回答您的问题:

在子组件中定义本地字段以保存FavoriteNumber参数属性的值,如下所示:

 @code 
  {
       [Parameter]
       public int FavoriteNumber { get; set; }
       private int FavoriteNumberLocal = -1;
  }

注意:FavoriteNumberLocal 变量存储从父组件传递的值。它允许您在本地存储并检查其值是否已更改,并相应地决定是否调用 Web Api 端点(再次强调,不应该这样做)。
protected override async Task OnParametersSetAsync()
{
    if( FavoriteNumberLocal != FavoriteNumber)
    { 
         FavoriteNumberLocal = FavoriteNumber;

         infoAboutFavoriteNumber = await ApiService.GetAsync<string>(id: 
         FavoriteNumberLocal.ToString());
    }
}  

请阅读此问题的最后两条评论。


有两个EditForms的原因是这些组件完全是模拟出来的,为了能够清晰地提出问题。它没有经过代码审查的好处。实际的代码确实考虑了组件的成本。 :) - Chris
你的子组件不应该执行任何 API 调用。如果你想在两个不同的页面上共享子组件,但是需要进行相同的 API 调用,那么该如何正确处理呢?如果每个父组件都进行相同的 API 调用,那么就会出现代码重复的问题,对吗? - undefined

3
您可以使用私有int实现自己的状态逻辑。
这比再次调用API要便宜得多。
<div>@infoAboutFavoriteNumber</div>

@code {
    [Parameter]
    public int FavoriteNumber { get; set; }

    [Parameter]
    public Book FavoriteBook { get; set; }

    private string infoAboutFavoriteNumber = "";

    private int currentNumber = -1; // some invalid value

    protected override async Task OnParametersSetAsync()
    {
      if (currentNumber != FavoriteNumber)
      {
        currentNumber = FavoriteNumber;
        infoAboutFavoriteNumber = await ApiService.GetAsync<string>(id: FavoriteNumber.ToString());
      }
    }
}

2
我不认为将这个逻辑放入OnParametersSetAsync()方法中是一种不好的做法。但是有一种方法可以防止它进行过多的API调用。我会创建一个私有变量来存储公共参数的值,然后每次调用OnParametersSetAsync()方法时,比较这两个变量,如果它们相同,则不进行API调用,如果它们不同,则进行API调用,并在完成后将私有变量赋值给公共参数的值。为了解决组件第一次调用该方法的情况,我可能会将私有变量赋值为默认值-1,因为通常ID值不是负数。但是基本上我会将其分配给永远不会等于任何传递作为参数的值的值。否则,第一次调用时,您的API可能实际上不会被调用。以下是一个例子:
<div>@infoAboutFavoriteNumber</div>

@code {
    [Parameter]
    public int FavoriteNumber { get; set; }
    private int CurrentFavoriteNumber  { get; set; } = -1; 

    [Parameter]
    public Book FavoriteBook { get; set; }

    private string infoAboutFavoriteNumber = "";

    protected override async Task OnParametersSetAsync()
    {
        if (FavoriteNumber != CurrentFavoriteNumber)
        {
            infoAboutFavoriteNumber = await ApiService.GetAsync<string>(id: FavoriteNumber.ToString());
            CurrentFavoriteNumber = FavoriteNumber;
        }
    }
}

1

您可以像其他建议一样引入本地字段并比较其值,或者在SetParametersAsync中捕获旧值在基本场景中可以工作。

但是,如果:

  • 参数变化太快?您将获得并发请求,响应可能以错误的顺序到达。
  • 您离开页面,响应稍后到达?
  • 您想延迟或限制参数更改,例如当参数绑定到用户输入时。

反应式扩展(IObservable)正是为处理此类情况而设计的。在 Angular(非常类似于 Blazor)中,RxJS 是一等公民。

在 Blazor 中,只需将参数转换为 IObservable,使用 RX 操作符处理它,而无需引入自己的本地变量。

readonly Subject<Unit> _parametersSet = new ();

protected override Task OnParametersSetAsync()
{
    _parametersSet.OnNext(Unit.Default); //turn OnParametersSetAsync into Observable stream
     return base.OnParametersSetAsync();
}


[Parameter] public int FavoriteNumber { get; set; }

protected override void OnInitialized()
{
    _parametersSet.Select(_ => FavoriteNumber) //turn parameter into Observable
        .DistinctUntilChanged() //detect changes
        .Select(value => Observable.FromAsync(cancellationToken => 
        {
            Console.WriteLine($"FavoriteNumber has changed: {value}");
            infoAboutFavoriteNumber = await ApiService.GetAsync(value, cancellationToken);
        })
        .Switch() //take care of concurrency
        .Subscribe();
}

很好的一点是,您可以创建一个可重用的类或帮助方法,其中包含所有样板文件。您只需指定参数和异步方法,例如:

Loader.Create(ObserveParameter(() => FavoriteNumber), LoadAsync);

更多阅读,请查看以下内容:


1
您面临的是一个常见问题:在UI中进行数据和数据访问活动。事情往往会变得混乱!在这个答案中,我将数据与组件分开处理。数据和数据访问驻留在依赖注入服务中。
我还取消了EditForm,因为您实际上并没有使用它,并将Select更改为简单选择,以便我们可以捕获更新、更新模型并触发服务中的数据检索。这也意味着模型更新后组件会重新渲染。Blazor UI事件处理程序OnChanged事件在调用NumberChanged后调用StateHasChanged
首先是我们喜爱的数据类。
public class MyFavourites
{
    public int FavouriteNumber { get; set; }    

    public string FavouriteNumberInfo { get; set; } = string.Empty;
}

第二步是创建一个DI服务来保存我们的收藏夹数据和数据存储操作。

namespace Server;

public class MyFavouritesViewService
{
    public MyFavourites Favourites { get; private set; } = new MyFavourites();

    public async Task GetFavourites()
    {
        // Emulate a database get
        await Task.Delay(100);
        Favourites = new MyFavourites { FavouriteNumber = 2, FavouriteNumberInfo = "The number is 2" };
    }

    public async Task SaveFavourites()
    {
        // Emulate a database save
        await Task.Delay(100);
        // Save code here
    }

    public async Task GetNewNumberInfo(int number)
    {
        if (number != Favourites.FavouriteNumber)
        {
            // Emulate a database get
            await Task.Delay(100);
            Favourites.FavouriteNumberInfo = $"The number is {number}";
            Favourites.FavouriteNumber = number;
        }
    }
}

接下来在程序中注册服务:

builder.Services.AddScoped<MyFavouritesViewService>();

组件:
<h3>MyFavouriteNumber is @this.Favourites.FavouriteNumber</h3>

<h3>MyFavouriteNumber info is @this.Favourites.FavouriteNumberInfo</h3>

@code {
    [Parameter][EditorRequired] public MyFavourites Favourites { get; set; } = new MyFavourites();
}

最后是页面。注意我使用OwningComponentBaseMyFavouritesViewService的作用范围与组件生命周期绑定。

@page "/favourites"
@page "/"
@inherits OwningComponentBase<MyFavouritesViewService>

@namespace Server

<h3>Favourite Number</h3>

<div class="p-5">
        <select class="form-select" @onchange="NumberChanged">
            @foreach (var option in options)
        {
            if (option.Key == this.Service.Favourites.FavouriteNumber)
            {
                <option selected value="@option.Key">@option.Value</option>
            }
            else
            {
                <option value="@option.Key">@option.Value</option>
            }
        }
        </select>
<div>
    <button class="btn btn-success" @onclick="SaveFavourites">Save</button>
</div>
</div>
<MyFavouriteNumber Favourites=this.Service.Favourites />

@code {
    
    private Dictionary<int, string> options = new Dictionary<int, string>
    {
        {0, "Zero"},
        {1, "One"},
        {2, "Two"},
        {3, "Three"},
    };

    //  Use OnInitializedAsync to get the original values from the data store
    protected async override Task OnInitializedAsync()
    =>  await this.Service.GetFavourites();

    // Demo to show saving
    private async Task SaveFavourites()
        => await this.Service.SaveFavourites();


    // Async setup ensures GetNewNumberInfo runs to completion
    // before StatehasChanged is called by the Handler
    // Renderer the checks what's changed and calls SetParamaterAsync 
    // on MyFavouriteNumber because FavouriteNumber has changed
    private async Task NumberChanged(ChangeEventArgs e)
    {
        if (int.TryParse(e.Value?.ToString(), out int value))
            await this.Service.GetNewNumberInfo(value);
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接