ASP.NET Core Razor component rendering

Note

This isn't the latest version of this article. For the current release, see the ASP.NET Core 8.0 version of this article.

This article explains Razor component rendering in ASP.NET Core Blazor apps, including when to call StateHasChanged to manually trigger a component to render.

Throughout this article, the terms server/server-side and client/client-side are used to distinguish locations where app code executes:

  • Server/server-side: Interactive server-side rendering (interactive SSR) of a Blazor Web App.
  • Client/client-side
    • Client-side rendering (CSR) of a Blazor Web App.
    • A Blazor WebAssembly app.

Documentation component examples usually don't configure an interactive render mode with an @rendermode directive in the component's definition file (.razor):

  • In a Blazor Web App, the component must have an interactive render mode applied, either in the component's definition file or inherited from a parent component. For more information, see ASP.NET Core Blazor render modes.

  • In a standalone Blazor WebAssembly app, the components function as shown and don't require a render mode because components always run interactively on WebAssembly in a Blazor WebAssembly app.

When using the Interactive WebAssembly or Interactive Auto render modes, component code sent to the client can be decompiled and inspected. Don't place private code, app secrets, or other sensitive information in client-rendered components.

  • Server/server-side
    • The Server project of a hosted Blazor WebAssembly app.
    • A Blazor Server app.
  • Client/client-side
    • The Client project of a hosted Blazor WebAssembly app.
    • A Blazor WebAssembly app.

For guidance on the purpose and locations of files and folders, see ASP.NET Core Blazor project structure, which also describes the location of the Blazor start script and the location of <head> and <body> content.

The best way to run the demonstration code is to download the BlazorSample_{PROJECT TYPE} sample apps from the Blazor samples GitHub repository that matches the version of .NET that you're targeting. Not all of the documentation examples are currently in the sample apps, but an effort is currently underway to move most of the .NET 8 article examples into the .NET 8 sample apps. This work will be completed in the first quarter of 2024.

Rendering conventions for ComponentBase

Components must render when they're first added to the component hierarchy by a parent component. This is the only time that a component must render. Components may render at other times according to their own logic and conventions.

By default, Razor components inherit from the ComponentBase base class, which contains logic to trigger rerendering at the following times:

Components inherited from ComponentBase skip rerenders due to parameter updates if either of the following are true:

Control the rendering flow

In most cases, ComponentBase conventions result in the correct subset of component rerenders after an event occurs. Developers aren't usually required to provide manual logic to tell the framework which components to rerender and when to rerender them. The overall effect of the framework's conventions is that the component receiving an event rerenders itself, which recursively triggers rerendering of descendant components whose parameter values may have changed.

For more information on the performance implications of the framework's conventions and how to optimize an app's component hierarchy for rendering, see ASP.NET Core Blazor performance best practices.

Streaming rendering

Use streaming rendering with interactive server-side rendering (interactive SSR) to stream content updates on the response stream and improve the user experience for components that perform long-running asynchronous tasks to fully render.

For example, consider a component that makes a long-running database query or web API call to render data when the page loads. Normally, asynchronous tasks executed as part of rendering a server-side component must complete before the rendered response is sent, which can delay loading the page. Any significant delay in rendering the page harms the user experience. To improve the user experience, streaming rendering initially renders the entire page quickly with placeholder content while asynchronous operations execute. After the operations are complete, the updated content is sent to the client on the same response connection and patched into the DOM.

Streaming rendering requires the server to avoid buffering the output. The response data must flow to the client as the data is generated. For hosts that enforce buffering, streaming rendering degrades gracefully, and the page loads without streaming rendering.

To stream content updates when using static server-side rendering (static SSR), apply the [StreamRendering(true)] attribute to the component. Streaming rendering must be explicitly enabled because streamed updates may cause content on the page to shift. Components without the attribute automatically adopt streaming rendering if the parent component uses the feature. Pass false to the attribute in a child component to disable the feature at that point and further down the component subtree. The attribute is functional when applied to components supplied by a Razor class library.

The following example is based on the Weather component in an app created from the Blazor Web App project template. The call to Task.Delay simulates retrieving weather data asynchronously. The component initially renders placeholder content ("Loading...") without waiting for the asynchronous delay to complete. When the asynchronous delay completes and the weather data content is generated, the content is streamed to the response and patched into the weather forecast table.

Weather.razor:

@page "/weather"
@attribute [StreamRendering(true)]

...

@if (forecasts == null)
{
    <p><em>Loading...</em></p>
}
else
{
    <table class="table">
        ...
        <tbody>
            @foreach (var forecast in forecasts)
            {
                <tr>
                    <td>@forecast.Date.ToShortDateString()</td>
                    <td>@forecast.TemperatureC</td>
                    <td>@forecast.TemperatureF</td>
                    <td>@forecast.Summary</td>
                </tr>
            }
        </tbody>
    </table>
}

@code {
    ...

    private WeatherForecast[]? forecasts;

    protected override async Task OnInitializedAsync()
    {
        await Task.Delay(500);

        ...

        forecasts = ...
    }
}

Suppress UI refreshing (ShouldRender)

ShouldRender is called each time a component is rendered. Override ShouldRender to manage UI refreshing. If the implementation returns true, the UI is refreshed.

Even if ShouldRender is overridden, the component is always initially rendered.

ControlRender.razor:

@page "/control-render"

<PageTitle>Control Render</PageTitle>

<h1>Control Render Example</h1>

<label>
    <input type="checkbox" @bind="shouldRender" />
    Should Render?
</label>

<p>Current count: @currentCount</p>

<p>
    <button @onclick="IncrementCount">Click me</button>
</p>

@code {
    private int currentCount = 0;
    private bool shouldRender = true;

    protected override bool ShouldRender()
    {
        return shouldRender;
    }

    private void IncrementCount()
    {
        currentCount++;
    }
}
@page "/control-render"

<label>
    <input type="checkbox" @bind="shouldRender" />
    Should Render?
</label>

<p>Current count: @currentCount</p>

<p>
    <button @onclick="IncrementCount">Click me</button>
</p>

@code {
    private int currentCount = 0;
    private bool shouldRender = true;

    protected override bool ShouldRender()
    {
        return shouldRender;
    }

    private void IncrementCount()
    {
        currentCount++;
    }
}
@page "/control-render"

<label>
    <input type="checkbox" @bind="shouldRender" />
    Should Render?
</label>

<p>Current count: @currentCount</p>

<p>
    <button @onclick="IncrementCount">Click me</button>
</p>

@code {
    private int currentCount = 0;
    private bool shouldRender = true;

    protected override bool ShouldRender()
    {
        return shouldRender;
    }

    private void IncrementCount()
    {
        currentCount++;
    }
}
@page "/control-render"

<label>
    <input type="checkbox" @bind="shouldRender" />
    Should Render?
</label>

<p>Current count: @currentCount</p>

<p>
    <button @onclick="IncrementCount">Click me</button>
</p>

@code {
    private int currentCount = 0;
    private bool shouldRender = true;

    protected override bool ShouldRender()
    {
        return shouldRender;
    }

    private void IncrementCount()
    {
        currentCount++;
    }
}
@page "/control-render"

<label>
    <input type="checkbox" @bind="shouldRender" />
    Should Render?
</label>

<p>Current count: @currentCount</p>

<p>
    <button @onclick="IncrementCount">Click me</button>
</p>

@code {
    private int currentCount = 0;
    private bool shouldRender = true;

    protected override bool ShouldRender()
    {
        return shouldRender;
    }

    private void IncrementCount()
    {
        currentCount++;
    }
}

For more information on performance best practices pertaining to ShouldRender, see ASP.NET Core Blazor performance best practices.

When to call StateHasChanged

Calling StateHasChanged allows you to trigger a render at any time. However, be careful not to call StateHasChanged unnecessarily, which is a common mistake that imposes unnecessary rendering costs.

Code shouldn't need to call StateHasChanged when:

  • Routinely handling events, whether synchronously or asynchronously, since ComponentBase triggers a render for most routine event handlers.
  • Implementing typical lifecycle logic, such as OnInitialized or OnParametersSetAsync, whether synchronously or asynchronously, since ComponentBase triggers a render for typical lifecycle events.

However, it might make sense to call StateHasChanged in the cases described in the following sections of this article:

An asynchronous handler involves multiple asynchronous phases

Due to the way that tasks are defined in .NET, a receiver of a Task can only observe its final completion, not intermediate asynchronous states. Therefore, ComponentBase can only trigger rerendering when the Task is first returned and when the Task finally completes. The framework can't know to rerender a component at other intermediate points, such as when an IAsyncEnumerable<T> returns data in a series of intermediate Tasks. If you want to rerender at intermediate points, call StateHasChanged at those points.

Consider the following CounterState1 component, which updates the count four times each time the IncrementCount method executes:

  • Automatic renders occur after the first and last increments of currentCount.
  • Manual renders are triggered by calls to StateHasChanged when the framework doesn't automatically trigger rerenders at intermediate processing points where currentCount is incremented.

CounterState1.razor:

@page "/counter-state-1"

<PageTitle>Counter State 1</PageTitle>

<h1>Counter State Example 1</h1>

<p>
    Current count: @currentCount
</p>

<p>
    <button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
</p>

@code {
    private int currentCount = 0;

    private async Task IncrementCount()
    {
        currentCount++;
        // Renders here automatically

        await Task.Delay(1000);
        currentCount++;
        StateHasChanged();

        await Task.Delay(1000);
        currentCount++;
        StateHasChanged();

        await Task.Delay(1000);
        currentCount++;
        // Renders here automatically
    }
}
@page "/counter-state-1"

<p>
    Current count: @currentCount
</p>

<p>
    <button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
</p>

@code {
    private int currentCount = 0;

    private async Task IncrementCount()
    {
        currentCount++;
        // Renders here automatically

        await Task.Delay(1000);
        currentCount++;
        StateHasChanged();

        await Task.Delay(1000);
        currentCount++;
        StateHasChanged();

        await Task.Delay(1000);
        currentCount++;
        // Renders here automatically
    }
}
@page "/counter-state-1"

<p>
    Current count: @currentCount
</p>

<p>
    <button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
</p>

@code {
    private int currentCount = 0;

    private async Task IncrementCount()
    {
        currentCount++;
        // Renders here automatically

        await Task.Delay(1000);
        currentCount++;
        StateHasChanged();

        await Task.Delay(1000);
        currentCount++;
        StateHasChanged();

        await Task.Delay(1000);
        currentCount++;
        // Renders here automatically
    }
}
@page "/counter-state-1"

<p>
    Current count: @currentCount
</p>

<p>
    <button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
</p>

@code {
    private int currentCount = 0;

    private async Task IncrementCount()
    {
        currentCount++;
        // Renders here automatically

        await Task.Delay(1000);
        currentCount++;
        StateHasChanged();

        await Task.Delay(1000);
        currentCount++;
        StateHasChanged();

        await Task.Delay(1000);
        currentCount++;
        // Renders here automatically
    }
}
@page "/counter-state-1"

<p>
    Current count: @currentCount
</p>

<p>
    <button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
</p>

@code {
    private int currentCount = 0;

    private async Task IncrementCount()
    {
        currentCount++;
        // Renders here automatically

        await Task.Delay(1000);
        currentCount++;
        StateHasChanged();

        await Task.Delay(1000);
        currentCount++;
        StateHasChanged();

        await Task.Delay(1000);
        currentCount++;
        // Renders here automatically
    }
}

Receiving a call from something external to the Blazor rendering and event handling system

ComponentBase only knows about its own lifecycle methods and Blazor-triggered events. ComponentBase doesn't know about other events that may occur in code. For example, any C# events raised by a custom data store are unknown to Blazor. In order for such events to trigger rerendering to display updated values in the UI, call StateHasChanged.

Consider the following CounterState2 component that uses System.Timers.Timer to update a count at a regular interval and calls StateHasChanged to update the UI:

  • OnTimerCallback runs outside of any Blazor-managed rendering flow or event notification. Therefore, OnTimerCallback must call StateHasChanged because Blazor isn't aware of the changes to currentCount in the callback.
  • The component implements IDisposable, where the Timer is disposed when the framework calls the Dispose method. For more information, see ASP.NET Core Razor component lifecycle.

Because the callback is invoked outside of Blazor's synchronization context, the component must wrap the logic of OnTimerCallback in ComponentBase.InvokeAsync to move it onto the renderer's synchronization context. This is equivalent to marshalling to the UI thread in other UI frameworks. StateHasChanged can only be called from the renderer's synchronization context and throws an exception otherwise:

System.InvalidOperationException: 'The current thread is not associated with the Dispatcher. Use InvokeAsync() to switch execution to the Dispatcher when triggering rendering or component state.'

CounterState2.razor:

@page "/counter-state-2"
@using System.Timers
@implements IDisposable

<PageTitle>Counter State 2</PageTitle>

<h1>Counter State Example 2</h1>

<p>
    This counter demonstrates <code>Timer</code> disposal.
</p>

<p>
    Current count: @currentCount
</p>

@code {
    private int currentCount = 0;
    private Timer timer = new(1000);

    protected override void OnInitialized()
    {
        timer.Elapsed += (sender, eventArgs) => OnTimerCallback();
        timer.Start();
    }

    private void OnTimerCallback()
    {
        _ = InvokeAsync(() =>
        {
            currentCount++;
            StateHasChanged();
        });
    }

    public void Dispose() => timer.Dispose();
}
@page "/counter-state-2"
@using System.Timers
@implements IDisposable

<h1>Counter with <code>Timer</code> disposal</h1>

<p>
    Current count: @currentCount
</p>

@code {
    private int currentCount = 0;
    private Timer timer = new(1000);

    protected override void OnInitialized()
    {
        timer.Elapsed += (sender, eventArgs) => OnTimerCallback();
        timer.Start();
    }

    private void OnTimerCallback()
    {
        _ = InvokeAsync(() =>
        {
            currentCount++;
            StateHasChanged();
        });
    }

    public void Dispose() => timer.Dispose();
}
@page "/counter-state-2"
@using System.Timers
@implements IDisposable

<h1>Counter with <code>Timer</code> disposal</h1>

<p>
    Current count: @currentCount
</p>

@code {
    private int currentCount = 0;
    private Timer timer = new(1000);

    protected override void OnInitialized()
    {
        timer.Elapsed += (sender, eventArgs) => OnTimerCallback();
        timer.Start();
    }

    private void OnTimerCallback()
    {
        _ = InvokeAsync(() =>
        {
            currentCount++;
            StateHasChanged();
        });
    }

    public void Dispose() => timer.Dispose();
}
@page "/counter-state-2"
@using System.Timers
@implements IDisposable

<h1>Counter with <code>Timer</code> disposal</h1>

<p>
    Current count: @currentCount
</p>

@code {
    private int currentCount = 0;
    private Timer timer = new(1000);

    protected override void OnInitialized()
    {
        timer.Elapsed += (sender, eventArgs) => OnTimerCallback();
        timer.Start();
    }

    private void OnTimerCallback()
    {
        _ = InvokeAsync(() =>
        {
            currentCount++;
            StateHasChanged();
        });
    }

    public void Dispose() => timer.Dispose();
}
@page "/counter-state-2"
@using System.Timers
@implements IDisposable

<h1>Counter with <code>Timer</code> disposal</h1>

<p>
    Current count: @currentCount
</p>

@code {
    private int currentCount = 0;
    private Timer timer = new Timer(1000);

    protected override void OnInitialized()
    {
        timer.Elapsed += (sender, eventArgs) => OnTimerCallback();
        timer.Start();
    }

    private void OnTimerCallback()
    {
        _ = InvokeAsync(() =>
        {
            currentCount++;
            StateHasChanged();
        });
    }

    public void Dispose() => timer.Dispose();
}

To render a component outside the subtree that's rerendered by a particular event

The UI might involve:

  1. Dispatching an event to one component.
  2. Changing some state.
  3. Rerendering a completely different component that isn't a descendant of the component receiving the event.

One way to deal with this scenario is to provide a state management class, often as a dependency injection (DI) service, injected into multiple components. When one component calls a method on the state manager, the state manager raises a C# event that's then received by an independent component.

For approaches to manage state, see the following resources:

For the state manager approach, C# events are outside the Blazor rendering pipeline. Call StateHasChanged on other components you wish to rerender in response to the state manager's events.

The state manager approach is similar to the earlier case with System.Timers.Timer in the previous section. Since the execution call stack typically remains on the renderer's synchronization context, calling InvokeAsync isn't normally required. Calling InvokeAsync is only required if the logic escapes the synchronization context, such as calling ContinueWith on a Task or awaiting a Task with ConfigureAwait(false). For more information, see the Receiving a call from something external to the Blazor rendering and event handling system section.

WebAssembly loading progress indicator for Blazor Web Apps

A loading progress indicator isn't present in an app created from the Blazor Web App project template. A new loading progress indicator feature is planned for a future release of .NET. In the meantime, an app can adopt custom code to create a loading progress indicator. For more information, see ASP.NET Core Blazor startup.