Exploring Blazor Changes in .NET 8 - Interactive Components using Blazor Server

July 25, 2023 · 6 minute read · Tags: blazor

From .NET 8 onwards you’ll be able to use Blazor components on the server, just like you would MVC or Razor Pages, thanks to the new Server Side Rendering mode.

This will give you a way to build your app using Razor Components which are rendered on the server and return static HTML (to be displayed in the browser).

But web applications often need a little more interactivity than just static content in the browser.

Islands of Interactivity

Say, for example, you have an online store, with a product details page.

The product details page itself can be rendered on the server…

Details.razor

@page "/Products/{Id}"
@inject ProductStore Store

<h3>@details.Title</h3>

<p>@details.Description</p>
@code {
    private ProductDetails details;

    [Parameter]
    public string Id { get; set; }

    protected override Task OnParametersSetAsync()
    {
        details = Store.Get(Convert.ToInt32(Id));
        return base.OnParametersSetAsync();
    }

}

Note there’s a bug in the current preview which prevents a component from accepting non-string parameters, hence the need to take the Id as a string

Now say you want to show related products on this same page.

If you’ve taken a look at an online store like Amazon recently it seems there’s a near endless amount of related products for you to scroll through.

It doesn’t make much sense to try and grab the details of an extra 10, 50, 100 or more related products every time someone views this details page (especially as there’s a good chance they won’t look at them, and if they do they may only scroll through the first few products before jumping off to a different page).

It would be preferable to fetch these related products separately, after the main page has been rendered on the server and displayed in the browser.

For that we can create a dedicated RelatedProducts component, that fetches its own data.

RelatedProducts.razor

@using BlazorDemoApp.Shared.Data
@inject ProductStore Store

@if (related == null)
{
    <p>Loading...</p>
}
else
{
    @if (related.Data.Any())
    {
        <h2>Related Products</h2>

        <section class="flex-row d-flex gap-2">
            @if (currentPage > 0)
            {
                <button class="btn btn-outline-secondary" @onclick="PrevPage">&lt;</button>
            }
            
            @foreach (var item in related.Data)
            {
                <div class="p-2 rounded-2 border">
                    <p class="text-center">@item.Title</p>
                    <img src="images/@item.Image"/>
                </div>
            }
            
            @if (related.TotalPages > currentPage)
            {
                <button class="btn btn-outline-secondary" @onclick="NextPage">&gt;</button>
            }
        </section>
    }
}

This shows the first few related products, with buttons to navigate to the next/previous ‘page’ of results.

Here’s the code that powers this UI:

RelatedProducts.razor (Code)

@code {
    private RelatedProductsList? related;

    int currentPage = 0;

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

    protected override async Task OnInitializedAsync()
    {
        await LoadRelatedProducts();
    }

    private async Task NextPage()
    {
        currentPage++;
        await LoadRelatedProducts();
    }

    private async Task PrevPage()
    {
        currentPage--;
        await LoadRelatedProducts();
    }

    private async Task LoadRelatedProducts()
    {
        related = await Store.ListRelated(Id, currentPage);
    }

}

In this example we’ve injected an instance of IProductStore, which we can use to fetch the related products.

When the ‘Next Page’ button is clicked, the page number is incremented and the next batch of related products is fetched.

If the previous button is clicked the page number is decremented and the previous batch of related products is fetched.

But how can we use this with our Details page (which is being rendered on the server, via SSR)?

First we can declare an instance of the component, as we would with any other Blazor application.

Details.razor

@page "/Products/{Id}"
@inject ProductStore Store

<h3>@details.Title</h3>

<p>@details.Description</p>

<RelatedProducts Id="Convert.ToInt32(Id)" />

Again, the need to convert the Id string to an int is temporary and will be fixed in the next preview release.

Now this will work, in as much as we get a handy list of related products when we view this in the browser.

Product details page

This works because the Details.razor component, including any child components (RelatedProducts.razor in this case) is rendered on the server.

The resulting HTML is then returned to the browser.

Product details page source

But, the button to view the next page of results doesn’t work yet.

That’s because this component was rendered on the server but is no longer running anywhere to handle further interactions.

Make it interactive using Blazor Server

To fix that, we can make this RelatedProducts component interactive, using either Blazor WASM or Blazor Server.

Let’s try using Blazor Server:

<RelatedProducts Id="Convert.ToInt32(Id)" @rendermode="@RenderMode.Server" />

When we view this in the browser this time:

  • The component is rendered once on the server (pre-rendered)
  • Blazor Server then kicks in (and opens a socket connection between browser and server)
  • The component is rendered again via Blazor Server, making it fully interactive (with all interactions being handled via that socket connection)

This time, when we click the button to view the next page of results, it works as we’d expect.

Interactive Components Demo

Pre-rendering is optional

By default the component is pre-rendered on the server, then rendered again when Blazor Server takes over.

This provides a smooth experience in the browser because the initial response includes markup which the browser can render while the interactive version spins up.

But, in some cases, you might want to skip that initial render, which you can do using a slightly different syntax.

<RelatedProducts Id="Convert.ToInt32(Id)" @rendermode="@(new ServerRenderMode(false))" />

With this, the initial render (SSR) for Details.razor will include a placeholder for the RelatedProducts component, but no actual content.

The result is a visible delay in the UI (albeit for a very short time) while the component spins up, and fetches the data.

But what about using Blazor WASM?

The other option is to employ Blazor WASM, to effectively deploy this component to the browser and have it run there, via Web Assembly.

<RelatedProducts Id="Convert.ToInt32(Id)" @rendermode="@RenderMode.WebAssembly" />

In practice, making your app, and components ‘WASM-compatible’ work requires some tweaks to your project’s architecture.

We’ll explore those changes in the next post.

In Summary

For many apps Blazor SSR is going to make a lot of sense, especially for content which is largely ‘static’ in nature.

Where a little more interactivity is required, using Blazor Server to enable ‘islands of interactivity’ looks set to offer a low-friction option, with minimal changes needed to the underlying app.

Because SSR and Blazor Server both operate on the server, the same logic/data-fetching mechanisms should work just fine in both scenarios.

Blazor WASM is also an option, albeit with a little more work needed to make it work, not least because the components which need to run via WASM need to exist in a separate project (which can be deployed to the browser).

More on that in the next post.

Join the Practical ASP.NET Newsletter

Get every .NET 8 article first, delivered straight to your inbox.

I respect your email privacy. Unsubscribe with one click.

    Next up

    Finally! Improved Blazor Server reconnection UX
    .NET 9 changes how your Blazor Server app behaves when server connection is lost
    .NET 9 improves JavaScript module importing for Blazor
    .NET 9 ensures your users always get the latest version of your JS modules
    How to use .NET 9 to ensure users always get the latest version of your stylesheets
    .NET 9 changes how static files are served, and it solves a long-standing problem