blazor.net-8.0blazor-rendermode

Blazor InteractiveAuto and Best Practices with Lifecycle


We have a Blazor application that is using the newer InteractiveAuto rendering mode. We have started down a path, but have discovered an issue with our pages: none of them can be directly accessed.

First, a small explanation of what we are working with. We have Services that connect to an external API. This is how we get our data. Pretty standard.

We are using Blazored Session Storage to store a Selected Customer.

We are doing this to avoid needing to place the customer id in the route and then reload that data from the server over and over, and in our experience, storing it in memory was too volatile because if the circuit resets we lose the data - which happened way too frequently

our current approach on a page/component looks something like this:

protected override async Task OnInitializedAsync()
{
    //get selected customer from local storage

    //get data from API service using info from selected customer
}

If we come to that page via a NavLink or NavigationManager this all works very nicely.

But, when we try to hit the page directly by typing in the URL, We get a JSinterop error because Blazored is using JSinterop to access local storage, which is not available during static prerendering.

Is there a way to disable static prerendering on a page by page basis, or am I going to have to go through and rework all of my pages and components that rely on this selected customer information to do data retrieval in OnAfterRenderAsync - which is less ideal because page loads blank and then content blinks into existence depending on server lag and the time it takes for the data to return.


Solution

  • I ended up finding this discussion with a solution that worked for me.

    Note: It appears that Blazor in .NET 9.0 will include some level of access/information to the render process/location, so this is probably only needed for Blazor in .NET 8.0

    I created an interface in a shared project:

    public interface IRenderContext
    {
        /// <summary>
        /// Rendering from the Client project. Using HTTP request for connectivity.
        /// </summary>
        public bool IsClient { get; }
    
        /// <summary>
        /// Rendering from the Server project. Using WebSockets for connectivity.
        /// </summary>
        public bool IsServer { get; }
    
        /// <summary>
        /// Rendering from the Server project. Indicates if the response has started rendering.
        /// </summary>
        public bool IsPreRendering { get; }
    }
    
    

    In my WASM project, I created the following concrete implementation:

    public sealed class ClientRenderContext : IRenderContext
    {
        /// <inheritdoc/>
        public bool IsClient => true;
    
        /// <inheritdoc/>
        public bool IsServer => false;
    
        /// <inheritdoc/>
        public bool IsPreRendering => false;
    }
    

    And registered it in the WASM project DI:

    services.AddSingleton<IRenderContext, ClientRenderContext>();
    

    In my Server project, I created this concrete implementation:

    public sealed class ServerRenderContext(IHttpContextAccessor contextAccessor) : IRenderContext
    {
        /// <inheritdoc/>
        public bool IsClient => false;
    
        /// <inheritdoc/>
        public bool IsServer => true;
    
        /// <inheritdoc/>
        public bool IsPreRendering => !contextAccessor.HttpContext?.Response.HasStarted ?? false;
    }
    

    And registered in my server project DI:

    services.AddHttpContextAccessor();
    services.AddSingleton<IRenderContext, ServerRenderContext>();
    

    I have a base class that all components inherit from in either project:

    public abstract partial class BaseComponent : ComponentBase, IDisposable
    {
        [CascadingParameter] private ErrorHandlerComponent? ErrorHandler { get; set; }
    
        [Inject] private IRenderContext RenderContext { get; set; }
    
        protected bool IsPreRender => RenderContext?.IsPreRendering ?? false;
    
        protected CancellationTokenSource? CancellationTokenSource { get; set; }
    
        protected CancellationToken CancellationToken => (CancellationTokenSource ??= new()).Token;
    
        public new virtual async Task DispatchExceptionAsync(Exception exception)
        {
            if (ErrorHandler is not null)
            {
                await ErrorHandler.HandleExceptionAsync(exception);
            }
    
            try
            {
                /*
                 * Treats the supplied exception as being thrown by this component.
                 * This will cause the enclosing ErrorBoundary to transition into a
                 * failed state. If there is no enclosing ErrorBoundary, it will be
                 * regarded as an exception from the enclosing renderer.
                 */
    
                await base.DispatchExceptionAsync(exception);
            }
            catch (System.ArgumentException)
            {
                //this error thrown if no renderer found - just eat the error to prevent a cascading failure
            }
        }
    
        public virtual void Dispose()
        {
            if (CancellationTokenSource != null)
            {
                CancellationTokenSource.Cancel();
                CancellationTokenSource.Dispose();
                CancellationTokenSource = null;
            }
        }
    }
    

    This then allows me to write my Blazor components so they don't have to wait to be fully rendered before using interop to access data needed to go get data from my external API.

    MyPage.Razor

    @page "/my-page"
    @rendermode InteractiveAuto
    @inherits BaseComponent
    @layout MainLayout
    
    <!-- UI omitted for brevity -->
    

    MyPage.razor.cs

    public partial class MyPage
    {
        [Inject] private IMyInteropService MyInteropService { get; set; }
        [Inject] private IMyDataService MyDataService { get; set; }
    
        protected override async Task OnInitializedAsync()
        {
            try
            {
                if (!IsPreRender)
                {
                    var itemInLocalStorage = await MyInteropService.GetAsync(this.CancellationToken);
    
                    if (itemInLocalStorage is not null)
                    {
                        var myDataFromApi = await MyDataService .GetAsync(itemInLocalStorage.MyId, this.CancellationToken);
    
                        //Do something with the data used in the UI
                    }
                }
            }
            catch (Exception e)
            {
                await base.DispatchExceptionAsync(e);
            }
            finally
            {
                StateHasChanged();
            }
        }
    }
    

    Once I implemented this strategy in all of our components I could directly access any page by direct route.