Ciclo de vida de los componentes Blazor: la guía completa

· 5 min de lectura

He estado usando Blazor por un tiempo y, sinceramente, los métodos del ciclo de vida me confundieron al principio. ¿OnInitialized frente a OnInitializedAsync? ¿OnParametersSet frente a OnAfterRender? ¿Cuándo StateHasChanged activa realmente una nueva renderización? Después de muchas pruebas y errores, finalmente tengo un modelo mental sólido para todo ello.

El ciclo de vida de un vistazo

Cuando se procesa un componente Blazor, sigue estos métodos en orden:

  1. SetParametersAsync
  2. OnInitialized / OnInitializedAsync
  3. OnParametersSet / OnParametersSetAsync
  4. OnAfterRender / OnAfterRenderAsync
  5. Dispose / DisposeAsync

Repasemos cada uno.

Establecer parámetrosAsync

Este es el primer método llamado. Recibe el ParameterView sin formato del componente principal. La mayoría de las veces no se anula esto: Blazor maneja los parámetros de mapeo automáticamente. Pero si necesita manejo o validación de parámetros personalizados antes de asignarlos:

public override async Task SetParametersAsync(ParameterView parameters)
{
    // Custom logic before parameters are set
    if (parameters.TryGetValue<string>("Title", out var title))
    {
        Console.WriteLine($"Title is being set to: {title}");
    }

    await base.SetParametersAsync(parameters);
}

He usado esto exactamente una vez en un proyecto real. La mayoría de las veces te lo saltarás.

OnInitialized / OnInitializedAsync

Aquí es donde realiza su trabajo de configuración: carga datos, inicializa servicios, establece valores predeterminados. Se ejecuta una vez cuando se crea el componente por primera vez.

@code {
    private List<Product> products = new();

    protected override async Task OnInitializedAsync()
    {
        products = await Http.GetFromJsonAsync<List<Product>>("api/products");
    }
}

Una cosa que me hizo tropezar: en Blazor Server, OnInitializedAsync recibe llamadas dos veces durante el procesamiento previo. La primera vez durante la renderización previa del lado del servidor y la segunda vez cuando se establece la conexión SignalR. Si su llamada API es costosa, es posible que desee encargarse de eso:

private bool isPrerendering = true;

protected override async Task OnInitializedAsync()
{
    products = await Http.GetFromJsonAsync<List<Product>>("api/products");
    isPrerendering = false;
}

O mejor aún, use PersistentComponentState para evitar la doble llamada por completo.

OnParametersSet / OnParametersSetAsync

Esto se activa cada vez que el componente principal se vuelve a renderizar y pasa nuevos valores de parámetros. También se activa después de OnInitialized. Este es el lugar adecuado para reaccionar ante cambios de parámetros:

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

private int previousCategoryId;
private List<Product> products = new();

protected override async Task OnParametersSetAsync()
{
    if (CategoryId != previousCategoryId)
    {
        previousCategoryId = CategoryId;
        products = await Http.GetFromJsonAsync<List<Product>>(
            $"api/products?category={CategoryId}");
    }
}

La verificación de CategoryId != previousCategoryId es importante: sin ella, recargaría los datos cada vez que el padre se vuelva a renderizar, incluso si la categoría no cambiara.

OnAfterRender / OnAfterRenderAsync

Esto se activa después de que el componente se haya renderizado en el DOM. El parámetro firstRender te indica si es el renderizado inicial. Este es el lugar para las llamadas de JS Interop ya que los elementos DOM existen en este punto:

[Inject]
private IJSRuntime JS { get; set; }

protected override async Task OnAfterRenderAsync(bool firstRender)
{
    if (firstRender)
    {
        await JS.InvokeVoidAsync("initializeChart", chartElement);
    }
}

Utilizo mucho firstRender para evitar reiniciar las bibliotecas de JavaScript en cada renderizado. Si está configurando detectores de eventos, bibliotecas de gráficos o cualquier cosa que toque directamente el DOM, aquí es donde va.

Debería renderizar

Éste es menos conocido pero muy útil. Controla si un componente se vuelve a renderizar cuando se llama a StateHasChanged:

private bool shouldRender = true;

protected override bool ShouldRender() => shouldRender;

private void HeavyOperation()
{
    shouldRender = false;

    // Do a bunch of state changes without triggering renders
    for (int i = 0; i < 1000; i++)
    {
        items[i].Process();
    }

    shouldRender = true;
    StateHasChanged(); // Now render once with all changes
}

He usado esto para optimizar componentes que procesan listas grandes. En lugar de volver a renderizar cada cambio de elemento, agrupa las actualizaciones y las renderiza una vez al final.

Disponer/DisposeAsync

Cuando se elimina un componente de la interfaz de usuario, debe limpiar todos los recursos. Implementar IDisposable o IAsyncDisposable:

@implements IAsyncDisposable

@code {
    private Timer? timer;
    private IJSObjectReference? jsModule;

    protected override void OnInitialized()
    {
        timer = new Timer(OnTick, null, 0, 1000);
    }

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            jsModule = await JS.InvokeAsync<IJSObjectReference>("import", "./timer.js");
        }
    }

    public async ValueTask DisposeAsync()
    {
        timer?.Dispose();

        if (jsModule is not null)
        {
            await jsModule.DisposeAsync();
        }
    }
}

Elementos comunes que debe eliminar: temporizadores, suscripciones a eventos, referencias de módulos JS, CancellationTokenSource y cualquier servicio IDisposable que haya creado.

EstadoHaCambiadoEste no es un método de ciclo de vida, pero está estrechamente relacionado. Le dice a Blazor “oye, mi estado cambió, por favor vuelve a renderizarme”. Blazor lo llama automáticamente después de los controladores de eventos, pero a veces es necesario llamarlo manualmente, generalmente cuando el estado cambia desde fuera del flujo normal de eventos de Blazor:

private async Task StartPolling()
{
    while (!cts.Token.IsCancellationRequested)
    {
        data = await Http.GetFromJsonAsync<Data>("api/data");
        StateHasChanged(); // Manual call needed since this isn't a Blazor event
        await Task.Delay(5000, cts.Token);
    }
}

Una nota importante: si está actualizando desde un hilo en segundo plano en Blazor Server, use InvokeAsync:

await InvokeAsync(() =>
{
    data = newData;
    StateHasChanged();
});

La imagen completa

Este es el orden en el que sucede todo:

Component created
  └─ SetParametersAsync
       └─ OnInitialized / OnInitializedAsync
            └─ OnParametersSet / OnParametersSetAsync
                 └─ Render
                      └─ OnAfterRender(firstRender: true)

Parameter change from parent
  └─ SetParametersAsync
       └─ OnParametersSet / OnParametersSetAsync
            └─ ShouldRender?
                 └─ Render
                      └─ OnAfterRender(firstRender: false)

Component removed
  └─ Dispose / DisposeAsync

Una vez que tenga este flujo en su cabeza, la depuración de problemas del ciclo de vida se vuelve mucho más fácil.

Espero que os haya gustado el post! No dudes en contactarme en cualquier red social en @emimontesdeoca.

Recursos