Advanced Blazor State Management Using Fluxor, part 7 - Client-to-Client comms with SignalR
Published: 2021-05-08 23:14:36

> This is the seventh in a short series of blog posts where I will go beyond the introductory level and dig a bit deeper into using the Fluxor library in a Blazor Wasm project.

### Client-to-Client Communication

Consider a scenario where you want the state of your application in one user's browser client to affect the state of your application in another user's browser client. Perhaps you have a component that updates in real-time based on the activities of other users. Or maybe you want to notify a user that the record they are edited was just saved by another user, and they should refresh their data before continuing.

So far in this series everything we've done has dealt with the state of individual components, or with components sharing state with other components in the same browser instance. Actions can affect the state of other components in the same browser instance, and Effects can interact with a server or external API, but still each browser instance is independent and unaware of any other browser instances running the same application.

In order to accommodate the client-to-client scenario, we'll have to incorporate some other tool that allows the server to push messages to all of the clients that have the application loaded.

### Enter SignalR

As with most challenges, there are undoubtedly many ways to accomplish this. One of the simplest would be to incorporate [ASP.NET Core SignalR](https://github.com/dotnet/aspnetcore/tree/main/src/SignalR) into our Blazor Wasm client and server projects. 

> ASP.NET Core SignalR is a library for ASP.NET Core developers that makes it incredibly simple to add real-time web functionality to your applications. What is "real-time web" functionality? It's the ability to have your server-side code push content to the connected clients as it happens, in real-time.

There is a handy tutorial for incorporating SignalR into a Blazor WebAssembly application [in the official documentation here](https://docs.microsoft.com/en-us/aspnet/core/tutorials/signalr-blazor). I encourage you to at least glance through it before continuing here, because the rest of this post is going to assume at least a basic familiarity with SignalR and configuration. 

For this post, I want to take it a few steps further by incorporating it into the Fluxor Store setup we've been building.

### Broadcasting the Counter state

I will be adding a button to the Counter screen that, when clicked, will broadcast the counter's current value to all of the other clients. I'm triggering this with an explicit button click just for demonstration convenience; it could just as easily be triggered automatically by subscribing to an Action as we did with the Weather Forecast updates in [Part 4 of this series](https://dev.to/mr_eking/advanced-blazor-state-management-using-fluxor-part-4-3olm). 

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8a9x1vdmdfvk459ffpmx.png)

This "Broadcast" button will be disabled if the client loses connectivity to the server, and toast notifications will be used to indicate when broadcasted Counter values are received, similar to the previous few posts in the series.

### The CounterHub

The SignalR component that manages the connections and messaging on the server is called a `Hub`. So our first step is creating a Hub in our `BlazorWithFluxor.Server` project, which I am calling `CounterHub`. I first create a folder at the root of the project called `Hubs`, and add file named `CounterHub.cs` that contains the following class:

```csharp
using Microsoft.AspNetCore.SignalR;
using System.Threading.Tasks;

namespace BlazorWithFluxor.Server.Hubs
{
    public class CounterHub : Hub
    {
        public async Task SendCount(int count)
        {
            await Clients.Others.SendAsync("ReceiveCount", count);
        }
    }
}

```

This class derives from `Microsoft.AspNetCore.SignalR.Hub` and contains one method `SendCount` that clients will use to send a `count` value, and the method broadcasts the value to all of the _other_ clients via `Clients.Others.SendAsync("ReceiveCount", count)`. The first parameter of `"ReceiveCount"` represents the name of the message that clients will be listening for.

To activate the `CounterHub`, we need to call `AddSignalR` in the `ConfigureServices` method of `Startup.cs`, along with adding response compression like so:

```csharp
public void ConfigureServices(IServiceCollection services)
{
    services.AddSignalR();
    services.AddControllersWithViews();
    services.AddRazorPages();

    services.AddResponseCompression(opts =>
    {
        opts.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(
            new[] { "application/octet-stream" });
    });
}
```

The final piece for `Startup.cs` is to add an endpoint for the `CounterHub` in the `UseEndpoints` block:

```csharp
app.UseEndpoints(endpoints =>
{
    endpoints.MapRazorPages();
    endpoints.MapControllers();
    endpoints.MapHub<CounterHub>("/counterhub");
    endpoints.MapFallbackToFile("index.html");
});
```

And with that we have a working SignalR hub on the server.

### The CounterHubStore

For the `BlazorWithFluxor.Client` project, we begin by referencing the `Microsoft.AspNetCore.SignalR.Client` [NuGet package](https://www.nuget.org/packages/Microsoft.AspNetCore.SignalR.Client).

```
Install-Package Microsoft.AspNetCore.SignalR.Client
```

Next we'll create a Fluxor store for the CounterHub, which I will place in a `\Features\Hubs` folder. 

The `CounterHubState` will only need to keep track of whether or not it's connected to the server, which we'll use to enable/disable the Broadcast button. Since the client's `HubConnection` must reach out and communicate with the server's `Hub`, we will hold the `HubConnection` in the `CounterHubEffects` class. We will use an `EffectMethod` to configure the `HubConnection` and to communicate with the server `Hub`. With a handful of Actions to handle all of the interaction, our `CounterHubStore` looks like this:

```csharp
using Fluxor;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.SignalR.Client;
using System;
using System.Threading.Tasks;

namespace BlazorWithFluxor.Client.Features.Hubs.CounterHub
{
    public record CounterHubState
    {
        public bool Connected { get; init; }
    };

    public class CounterHubFeature : Feature<CounterHubState>
    {
        public override string GetName() => "CounterHub";

        protected override CounterHubState GetInitialState()
        {
            return new CounterHubState 
            {
                Connected = false
            };
        }
    }

    public static class CounterHubReducers 
    {
        [ReducerMethod]
        public static CounterHubState OnSetConnected(CounterHubState state, CounterHubSetConnectedAction action) 
        {
            return state with
            {
                Connected = action.Connected
            };
        }
    }

    public class CounterHubEffects
    {
        private readonly HubConnection _hubConnection;

        public CounterHubEffects(NavigationManager navigationManager)
        {
            _hubConnection = new HubConnectionBuilder()
                .WithUrl(navigationManager.ToAbsoluteUri("/counterhub"))
                .WithAutomaticReconnect()
                .Build();
        }

        [EffectMethod]
        public async Task SendCount(CounterHubSendCountAction action, IDispatcher dispatcher)
        {
            try
            {
                if (_hubConnection.State == HubConnectionState.Connected)
                {
                    await _hubConnection.SendAsync("SendCount", action.Count);
                }
                else 
                {
                    dispatcher.Dispatch(new CounterHubSendCountFailedAction("Not connected to hub."));
                }
            }
            catch (Exception ex)
            {
                dispatcher.Dispatch(new CounterHubSendCountFailedAction(ex.Message));
            }
        }

        [EffectMethod(typeof(CounterHubStartAction))]
        public async Task Start(IDispatcher dispatcher) 
        {
            await _hubConnection.StartAsync();

            _hubConnection.Reconnecting += (ex) => 
            {
                dispatcher.Dispatch(new CounterHubSetConnectedAction(false));
                return Task.CompletedTask;
            };

            _hubConnection.Reconnected += (connectionId) =>
            {
                dispatcher.Dispatch(new CounterHubSetConnectedAction(true));
                return Task.CompletedTask;
            };

            _hubConnection.On<int>("ReceiveCount", (count) => dispatcher.Dispatch(new CounterHubReceiveCountAction(count)));

            dispatcher.Dispatch(new CounterHubSetConnectedAction(true));
        }
    }

    public record CounterHubSetConnectedAction(bool Connected);
    public record CounterHubStartAction();
    public record CounterHubReceiveCountAction(int Count);
    public record CounterHubSendCountAction(int Count);
    public record CounterHubSendCountFailedAction(string Message);

}
```

> Note that the server `Hub` is notified via an EffectMethod that handles the `CounterHubSendCountAction` Action, and the `ReceiveCount` server notification dispatches a `CounterHubReceiveCountAction` Action on the client. Fully incorporated into the flux pattern.

I dispatch the `CounterHubStartAction` in the `MainLayout.razor` file, to connect to the server once the client is ready:

```csharp
@code { 
    protected override void OnInitialized()
    {
        base.OnInitialized();
        Dispatcher.Dispatch(new CounterHubStartAction());
    }
}
```

I subscribe to a few new Actions in the `Toaster.razor` component:

```csharp
protected override void OnInitialized()
{
    ...

    SubscribeToAction<CounterHubReceiveCountAction>(ShowCountReceivedToast);
    SubscribeToAction<CounterHubSendCountFailedAction>(ShowCountReceivedFailedToast);
    SubscribeToAction<CounterHubSetConnectedAction>(ShowHubConnectedToast);

    base.OnInitialized();
}
```

and

```csharp
private void ShowCountReceivedToast(CounterHubReceiveCountAction action)
{
    toastService.ShowInfo($"Count received: {action.Count}");
}

private void ShowCountReceivedFailedToast(CounterHubSendCountFailedAction action)
{
    toastService.ShowError($"Count could not be broadcast: {action.Message}");
}

private void ShowHubConnectedToast(CounterHubSetConnectedAction action)
{
    if (action.Connected)
    {
        toastService.ShowSuccess($"CounterHub connected!");
    }
    else 
    {
        toastService.ShowError($"CounterHub disconnected!");
    }
}
```

And finally I add the Broadcast button in the `Counter.razor` component:

```html
<button class="btn btn-outline-warning" 
        @onclick="SendCount" 
        disabled="@(!CounterHubState.Value.Connected)">
    Broadcast
</button>
```

and dispatch the Current Count when the button is clicked:

```csharp
private void SendCount()
{
    Dispatcher.Dispatch(new CounterHubSendCountAction(CounterState.Value.CurrentCount));
}
```

With that complete, we can run our client in multiple browser windows to see the cross-client magic happen. For this example I used Firefox as one client and Chrome as the other, both connected to the same instance of the Server application. 

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/geazsg9weibsj2cgg8or.gif)

If the persistent connection to the server hub is lost (in this example, I stopped the server host application), the `CounterHubStore` will react appropriately, disabling the Broadcast button and notifying the user.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xlnia8tjie1fyztcim3a.gif)

Using the default `WithAutomaticReconnect` configuration, the clients will attempt to reconnect, and will re-enable the Broadcast button if successful.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/x3muqpfiyowwvb1ximk8.gif)

This is what it looks like with multiple clients running at once:

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hbtxx3xf8yr41tsg9uwd.gif)

One additional thing to note about SignalR is that you don't have to host the server `Hub` yourself if you don't want to. You could easily create a client-only Blazor WebAssembly serverless application and use Microsoft's Azure-based [SignalR service](https://dotnet.microsoft.com/apps/aspnet/signalr/service) to host the `Hub` in a massively scalable environment. 

Please leave a comment if you have a question or suggestion. I'd love to hear from you if you found this useful.

In the meantime, happy coding!