Every Blazor component goes through a predictable sequence of events from creation to disposal. Understanding this lifecycle is essential for building components that behave correctly, manage resources properly, and deliver good performance. This guide walks through each lifecycle method with practical examples.

The Lifecycle Methods Overview

Blazor components have several lifecycle methods that you can override to run code at specific points:

  1. SetParametersAsync - Called when parameters are set or updated
  2. OnInitialized / OnInitializedAsync - Called once when the component is first initialized
  3. OnParametersSet / OnParametersSetAsync - Called after parameters are set
  4. OnAfterRender / OnAfterRenderAsync - Called after the component has rendered
  5. Dispose / DisposeAsync - Called when the component is removed from the UI

Let us explore each one in detail.

SetParametersAsync

This is the first method called when a component receives parameters from its parent. You rarely need to override this method, but it is useful when you need to handle parameters before they are assigned to properties:

@code {
    [Parameter]
    public string Title { get; set; } = string.Empty;

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

    private string _previousTitle = string.Empty;

    public override async Task SetParametersAsync(ParameterView parameters)
    {
        // Store previous value before parameters are set
        _previousTitle = Title;

        // Call base implementation to assign parameters to properties
        await base.SetParametersAsync(parameters);

        // Now parameters have been assigned
        if (Title != _previousTitle)
        {
            Console.WriteLine($"Title changed from '{_previousTitle}' to '{Title}'");
        }
    }
}

You can also manually process parameters without using the base implementation:

public override async Task SetParametersAsync(ParameterView parameters)
{
    // Manually extract parameters
    if (parameters.TryGetValue<string>(nameof(Title), out var title))
    {
        Title = title;
    }

    if (parameters.TryGetValue<int>(nameof(Count), out var count))
    {
        Count = count;
    }

    // Still need to call base for the lifecycle to continue
    await base.SetParametersAsync(ParameterView.Empty);
}

OnInitialized and OnInitializedAsync

These methods run once when the component is first created. This is where you should perform one-time initialization like loading data or setting up subscriptions:

@page "/products"
@inject IProductService ProductService

<h3>Products</h3>

@if (isLoading)
{
    <div class="spinner">Loading...</div>
}
else if (products is not null)
{
    <ul>
        @foreach (var product in products)
        {
            <li>@product.Name - @product.Price.ToString("C")</li>
        }
    </ul>
}

@code {
    private List<Product>? products;
    private bool isLoading = true;

    protected override void OnInitialized()
    {
        // Synchronous initialization
        Console.WriteLine("Component initialized");
    }

    protected override async Task OnInitializedAsync()
    {
        try
        {
            products = await ProductService.GetProductsAsync();
        }
        finally
        {
            isLoading = false;
        }
    }
}

There is an important distinction between OnInitialized and OnInitializedAsync. In Blazor Server, the component renders twice during prerendering. The first render happens on the server for prerendering, and the second render happens when the SignalR connection is established. Both renders call OnInitialized/OnInitializedAsync.

To handle this, you can check if the component has already been initialized:

@code {
    private List<Product>? products;
    private bool hasInitialized = false;

    protected override async Task OnInitializedAsync()
    {
        if (hasInitialized)
        {
            return;
        }

        products = await ProductService.GetProductsAsync();
        hasInitialized = true;
    }
}

Or you can use PersistentComponentState to persist data across prerendering:

@inject PersistentComponentState ApplicationState
@implements IDisposable

@code {
    private List<Product>? products;
    private PersistingComponentStateSubscription persistingSubscription;

    protected override async Task OnInitializedAsync()
    {
        persistingSubscription = ApplicationState.RegisterOnPersisting(PersistData);

        if (!ApplicationState.TryTakeFromJson<List<Product>>("products", out var restored))
        {
            products = await ProductService.GetProductsAsync();
        }
        else
        {
            products = restored;
        }
    }

    private Task PersistData()
    {
        ApplicationState.PersistAsJson("products", products);
        return Task.CompletedTask;
    }

    public void Dispose()
    {
        persistingSubscription.Dispose();
    }
}

OnParametersSet and OnParametersSetAsync

These methods are called every time parameters change, including after the initial OnInitialized methods. Use these for logic that should run whenever parameters are updated:

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

    private Product? product;
    private int previousProductId;

    protected override async Task OnParametersSetAsync()
    {
        // Only reload if the ProductId actually changed
        if (ProductId != previousProductId)
        {
            previousProductId = ProductId;
            product = await ProductService.GetProductAsync(ProductId);
        }
    }
}

Here is a more complete example showing a component that responds to parameter changes:

@typeparam TItem

<div class="paginated-list">
    <div class="items">
        @foreach (var item in displayedItems)
        {
            @ItemTemplate(item)
        }
    </div>
    <div class="pagination">
        <button @onclick="PreviousPage" disabled="@(currentPage == 1)">Previous</button>
        <span>Page @currentPage of @totalPages</span>
        <button @onclick="NextPage" disabled="@(currentPage == totalPages)">Next</button>
    </div>
</div>

@code {
    [Parameter]
    public IEnumerable<TItem> Items { get; set; } = Enumerable.Empty<TItem>();

    [Parameter]
    public int PageSize { get; set; } = 10;

    [Parameter]
    public RenderFragment<TItem> ItemTemplate { get; set; } = default!;

    private List<TItem> allItems = new();
    private List<TItem> displayedItems = new();
    private int currentPage = 1;
    private int totalPages = 1;

    protected override void OnParametersSet()
    {
        // Reset to page 1 when items change
        allItems = Items.ToList();
        totalPages = (int)Math.Ceiling(allItems.Count / (double)PageSize);
        currentPage = 1;
        UpdateDisplayedItems();
    }

    private void UpdateDisplayedItems()
    {
        displayedItems = allItems
            .Skip((currentPage - 1) * PageSize)
            .Take(PageSize)
            .ToList();
    }

    private void PreviousPage()
    {
        if (currentPage > 1)
        {
            currentPage--;
            UpdateDisplayedItems();
        }
    }

    private void NextPage()
    {
        if (currentPage < totalPages)
        {
            currentPage++;
            UpdateDisplayedItems();
        }
    }
}

OnAfterRender and OnAfterRenderAsync

These methods are called after the component has rendered to the DOM. This is where you can interact with JavaScript or perform operations that require the DOM to be present:

@inject IJSRuntime JS

<div @ref="chartContainer" class="chart-container"></div>

@code {
    private ElementReference chartContainer;
    private bool chartInitialized = false;

    [Parameter]
    public double[] Data { get; set; } = Array.Empty<double>();

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            // Initialize the chart library on first render
            await JS.InvokeVoidAsync("initializeChart", chartContainer, Data);
            chartInitialized = true;
        }
        else if (chartInitialized)
        {
            // Update chart data on subsequent renders
            await JS.InvokeVoidAsync("updateChart", chartContainer, Data);
        }
    }
}

The firstRender parameter is crucial for distinguishing between the initial render and subsequent updates. Here is another example showing focus management:

@code {
    private ElementReference inputElement;
    private bool shouldFocus = false;

    [Parameter]
    public bool AutoFocus { get; set; }

    protected override void OnParametersSet()
    {
        if (AutoFocus)
        {
            shouldFocus = true;
        }
    }

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (shouldFocus)
        {
            await inputElement.FocusAsync();
            shouldFocus = false;
        }
    }
}

Implementing IDisposable and IAsyncDisposable

Components should implement IDisposable or IAsyncDisposable to clean up resources when they are removed from the UI:

@implements IAsyncDisposable
@inject IJSRuntime JS

<div @ref="mapContainer" class="map-container"></div>

@code {
    private ElementReference mapContainer;
    private IJSObjectReference? mapModule;
    private IJSObjectReference? mapInstance;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            mapModule = await JS.InvokeAsync<IJSObjectReference>(
                "import", "./js/map.js");
            mapInstance = await mapModule.InvokeAsync<IJSObjectReference>(
                "createMap", mapContainer);
        }
    }

    public async ValueTask DisposeAsync()
    {
        if (mapInstance is not null)
        {
            await mapInstance.InvokeVoidAsync("destroy");
            await mapInstance.DisposeAsync();
        }

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

Here is an example with event subscriptions:

@implements IDisposable
@inject INotificationService NotificationService

<div class="notifications">
    @foreach (var notification in notifications)
    {
        <div class="notification @notification.Type">
            @notification.Message
        </div>
    }
</div>

@code {
    private List<Notification> notifications = new();

    protected override void OnInitialized()
    {
        NotificationService.OnNotification += HandleNotification;
    }

    private void HandleNotification(Notification notification)
    {
        notifications.Add(notification);
        InvokeAsync(StateHasChanged);
    }

    public void Dispose()
    {
        NotificationService.OnNotification -= HandleNotification;
    }
}

StateHasChanged and Re-rendering

Blazor automatically calls StateHasChanged after event handlers and lifecycle methods. However, you sometimes need to call it manually:

@implements IDisposable

<div class="timer">@elapsedSeconds seconds</div>

@code {
    private int elapsedSeconds = 0;
    private Timer? timer;

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

    private void OnTimerElapsed(object? state)
    {
        elapsedSeconds++;
        // Must call InvokeAsync because we are on a different thread
        InvokeAsync(StateHasChanged);
    }

    public void Dispose()
    {
        timer?.Dispose();
    }
}

ShouldRender for Performance

Override ShouldRender to control when a component re-renders:

@code {
    [Parameter]
    public string Value { get; set; } = string.Empty;

    private string previousValue = string.Empty;

    protected override bool ShouldRender()
    {
        // Only re-render if Value has changed
        if (Value == previousValue)
        {
            return false;
        }

        previousValue = Value;
        return true;
    }
}

Here is a more complex example:

@code {
    [Parameter]
    public RenderFragment ChildContent { get; set; } = default!;

    [Parameter]
    public bool IsExpanded { get; set; }

    [Parameter]
    public string Title { get; set; } = string.Empty;

    private bool previousIsExpanded;
    private string previousTitle = string.Empty;
    private bool forceRender = false;

    protected override void OnParametersSet()
    {
        // Force render if specific parameters changed
        forceRender = IsExpanded != previousIsExpanded || Title != previousTitle;
        previousIsExpanded = IsExpanded;
        previousTitle = Title;
    }

    protected override bool ShouldRender()
    {
        if (forceRender)
        {
            forceRender = false;
            return true;
        }
        return false;
    }
}

Complete Lifecycle Example

Here is a complete component demonstrating all lifecycle methods:

@page "/lifecycle-demo/{Id:int}"
@implements IAsyncDisposable
@inject ILogger<LifecycleDemo> Logger
@inject IDataService DataService

<h3>Lifecycle Demo - ID: @Id</h3>

@if (isLoading)
{
    <p>Loading...</p>
}
else if (data is not null)
{
    <div class="data-display" @ref="dataElement">
        <p>Name: @data.Name</p>
        <p>Description: @data.Description</p>
    </div>
}

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

    private DataModel? data;
    private bool isLoading = true;
    private int previousId;
    private ElementReference dataElement;
    private CancellationTokenSource? cts;

    public override async Task SetParametersAsync(ParameterView parameters)
    {
        Logger.LogInformation("SetParametersAsync called");
        await base.SetParametersAsync(parameters);
    }

    protected override void OnInitialized()
    {
        Logger.LogInformation("OnInitialized called");
        cts = new CancellationTokenSource();
    }

    protected override async Task OnInitializedAsync()
    {
        Logger.LogInformation("OnInitializedAsync called");
        await LoadDataAsync();
    }

    protected override void OnParametersSet()
    {
        Logger.LogInformation($"OnParametersSet called - Id: {Id}, Previous: {previousId}");
    }

    protected override async Task OnParametersSetAsync()
    {
        Logger.LogInformation("OnParametersSetAsync called");

        if (Id != previousId && previousId != 0)
        {
            await LoadDataAsync();
        }
        previousId = Id;
    }

    protected override void OnAfterRender(bool firstRender)
    {
        Logger.LogInformation($"OnAfterRender called - firstRender: {firstRender}");
    }

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        Logger.LogInformation($"OnAfterRenderAsync called - firstRender: {firstRender}");

        if (firstRender)
        {
            // Perform any JS interop here
        }
    }

    protected override bool ShouldRender()
    {
        Logger.LogInformation("ShouldRender called");
        return true;
    }

    private async Task LoadDataAsync()
    {
        isLoading = true;
        StateHasChanged();

        try
        {
            data = await DataService.GetDataAsync(Id, cts?.Token ?? default);
        }
        catch (OperationCanceledException)
        {
            Logger.LogInformation("Data loading was cancelled");
        }
        finally
        {
            isLoading = false;
        }
    }

    public async ValueTask DisposeAsync()
    {
        Logger.LogInformation("DisposeAsync called");

        if (cts is not null)
        {
            cts.Cancel();
            cts.Dispose();
        }
    }
}

Best Practices

  1. Use OnInitializedAsync for initial data loading, not the constructor.

  2. Check if parameters actually changed in OnParametersSetAsync before doing expensive operations.

  3. Use OnAfterRenderAsync with firstRender for JavaScript interop initialization.

  4. Always implement IDisposable or IAsyncDisposable when using timers, event subscriptions, or JavaScript object references.

  5. Use CancellationToken to cancel async operations when the component is disposed.

  6. Call StateHasChanged manually only when updating state outside of Blazor events.

  7. Use ShouldRender to optimize performance for components that render frequently.

Conclusion

Understanding the Blazor component lifecycle helps you write components that initialize correctly, update efficiently, and clean up properly. The lifecycle methods give you hooks into each phase of a component's existence, allowing you to manage state, handle parameters, interact with JavaScript, and dispose of resources appropriately.

The key points to remember:

  • OnInitializedAsync runs once for initial setup
  • OnParametersSetAsync runs every time parameters change
  • OnAfterRenderAsync is for JavaScript interop and DOM access
  • Always dispose of resources in Dispose/DisposeAsync
  • Use ShouldRender to prevent unnecessary re-renders

With these patterns, you can build Blazor components that are reliable, performant, and maintainable.