Skip to the content.

Part 6: Real-time UI in Blazor Apps

You already know about IState<T> - it was described in Part 3. It’s an abstraction that “tracks” the most current version of some Computed<T>. There are a few “flavors” of the IState - the most important ones are:

You can use these abstractions directly in your Blazor components, but usually it’s more convenient to use ComputedStateComponent<TState> and MixedStateComponent<TState, TMutableState> from Stl.Fusion.Blazor NuGet package. I’ll describe how they work further, but since the classes are tiny, the link to their source code might explain it even better:

StatefulComponentBase<T> (source)

Any StatefulComponentBase has State property, which can be any IState.

When initialized, it tries to resolve the state via ServiceProvider - unless was already assigned. And in addition to that, it attaches its own event handler (StateChanged delegate - don’t confuse it with Blazor’s StateHasChanged method) to all State’s events (by default):

protected override void OnInitialized()
{
    // ReSharper disable once ConstantNullCoalescingCondition
    State ??= CreateState();
    UntypedState.AddEventHandler(StateEventKind.All, StateChanged);
}

protected virtual TState CreateState()
    => Services.GetRequiredService<TState>();

And this is how the default StateChanged handler looks:

protected StateEventKind StateHasChangedTriggers { get; set; } = StateEventKind.Updated;

protected StatefulComponentBase()
{
    StateChanged = (_, eventKind) => {
        if ((eventKind & StateHasChangedTriggers) == 0)
            return;
        this.NotifyStateHasChanged();
    };
}

As you see, by default any StatefulComponentBase triggers StateHasChanged once its State gets updated.

Finally, it also disposes the state once the component gets disposed - unless its OwnsState property is set to false. And that’s nearly all it does.

ComputedStateComponent<T> (source)

This class tweaks a behavior of StatefulComponentBase to deal IComputedState<T>.

This is literally all of its code:

public abstract class ComputedStateComponent<TState> : StatefulComponentBase<IComputedState<TState>>
{
    protected ComputedStateComponentOptions Options { get; set; } =
        ComputedStateComponentOptions.SynchronizeComputeState
        | ComputedStateComponentOptions.RecomputeOnParametersSet;

    // State frequently depends on component parameters, so...
    protected override Task OnParametersSetAsync()
    {
        if (0 == (Options & ComputedStateComponentOptions.RecomputeOnParametersSet))
            return Task.CompletedTask;
        State.Recompute();
        return Task.CompletedTask;
    }

    protected virtual ComputedState<TState>.Options GetStateOptions()
        => new();

    protected override IComputedState<TState> CreateState()
    {
        async Task<TState> SynchronizedComputeState(IComputedState<TState> _, CancellationToken cancellationToken)
        {
            // Synchronizes ComputeState call as per:
            // https://github.com/servicetitan/Stl.Fusion/issues/202
            var ts = TaskSource.New<TState>(false);
            await InvokeAsync(async () => {
                try {
                    ts.TrySetResult(await ComputeState(cancellationToken));
                }
                catch (OperationCanceledException) {
                    ts.TrySetCanceled();
                }
                catch (Exception e) {
                    ts.TrySetException(e);
                }
            });
            return await ts.Task.ConfigureAwait(false);
        }

        return StateFactory.NewComputed(GetStateOptions(),
            0 != (Options & ComputedStateComponentOptions.SynchronizeComputeState)
            ? SynchronizedComputeState
            : (_, ct) => ComputeState(ct));
    }

    protected abstract Task<TState> ComputeState(CancellationToken cancellationToken);
}

It doesn’t try to resolve the state via DI container, but constructs it using IStateFactory - and moreover:

So to have a component that automatically updates once the output of some Compute Service (or a set of such services) changes, all you need is to:

A good example of such component is Counter.razor from “HelloBlazorServer” example - check out its source code. Note that it already computes a complex value using two compute methods (CounterService.GetCounterAsync and GetMomentsAgoAsync):

protected override async Task<string> ComputeState(CancellationToken cancellationToken)
{
    var (count, changeTime) = await CounterService.Get();
    var momentsAgo = await Time.GetMomentsAgo(changeTime);
    return $"{count}, changed {momentsAgo}";
}

MixedStateComponent<T, TLocals> (source)

It’s pretty common for UI components to have its own (local) state (e.g. a text entered into a few form fields) and compute their State using some values from this local state - in other words, to have their State dependent on its local state.

There are a few ways to enforce State recomputation in such cases:

  1. If all you use is component parameters, State recomputation will happen automatically if ComputedStateComponentOptions.RecomputeOnParametersSet option is on (and that’s the default).
  2. You may also use component fields and call State.Recompute() to trigger its invalidation and recomputation w/o an update delay. State.Invalidate() will work as well, but in this case the recomputation will happen with usual update delay.
  3. Wrap full local state into e.g. IMutableState<T> MutableState and use it in ComputeState via var locals = await MutableState.Use(). As you might remember from Part 3, MutableState.Use is the same as MutableState.Computed.Use, and it makes state a dependency of what’s computed now, so once MutableState gets changed, the recomputation of State will happen automatically. Though if you need to nullify the update delay in this case, it’s going to be a bit more complex.

MixedStateComponent<TState, TMutableState> is a built-in implementation of option 3:

Check out its 30 lines of code to see how it works.

Real-time UI in Server-Side Blazor apps

As you might guess, all you need is to:

Your server-side web host configuration should include at least these parts:

public void ConfigureServices(IServiceCollection services)
{
    // Fusion services
    var fusion = services.AddFusion();

    // ASP.NET Core / Blazor services 
    services.AddRazorPages();
    services.AddServerSideBlazor(o => o.DetailedErrors = true);
}

// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    app.UseStaticFiles();
    app.UseRouting();
    app.UseEndpoints(endpoints =>
    {
        endpoints.MapBlazorHub();
        endpoints.MapFallbackToPage("/_Host");
    });
}

Real-time UI in Blazor WebAssembly apps

If you read about Compute Service Clients in Part 4, you probably already know that WASM case actually isn’t that different:

So your server-side web host configuration should include these parts:

public void ConfigureServices(IServiceCollection services)
{
    // Fusion services
    var fusion = services.AddFusion();
    fusion.AddWebServer();
    
    // ASP.NET Core / Blazor services 
    services.AddRazorPages();
    services.AddServerSideBlazor(o => o.DetailedErrors = true);
}

public void Configure(IApplicationBuilder app, ILogger<Startup> log)
{
    if (Env.IsDevelopment()) {
        app.UseWebAssemblyDebugging(); // Only if you need this
    }
    app.UseWebSockets(new WebSocketOptions() {
        KeepAliveInterval = TimeSpan.FromSeconds(30), // You can change this
    });

    // Static files
    app.UseBlazorFrameworkFiles(); // Needed for Blazor WASM

    // Endpoints
    app.UseRouting();
    app.UseEndpoints(endpoints => {
        endpoints.MapRpcWebSocketServer();
        endpoints.MapFallbackToPage("/_Host"); // Typically needed for Blazor WASM
    });
}

And your client-side DI container configuration should look as follows:

public static Task Main(string[] args)
{
    var builder = WebAssemblyHostBuilder.CreateDefault(args);
    ConfigureServices(builder.Services, builder);
    builder.RootComponents.Add<App>("app");
    var host = builder.Build();
    // Blazor host doesn't start IHostedService-s by default,
    // so let's start them "manually" here
    host.Services.HostedServices().Start();
    return host.RunAsync();
}

public static void ConfigureServices(IServiceCollection services, WebAssemblyHostBuilder builder)
{
    var baseUri = new Uri(builder.HostEnvironment.BaseAddress);
    var fusion = services.AddFusion();
    fusion.Rpc.AddWebSocketClient(baseUri);
}

Real-time UI in Blazor Hybrid apps

As you might guess, nothing prevents you from using both of above approaches to implement Blazor apps that support both Server-Side Blazor (SSB) and Blazor WebAssembly modes.

All you need is to:

Check out Blazor Sample to see how all of this works together.

Next: Part 7 » | Tutorial Home