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 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. 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.
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:
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:
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:
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.
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:
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 theCounterHubSendCountAction
Action, and theReceiveCount
server notification dispatches aCounterHubReceiveCountAction
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:
@code {
protected override void OnInitialized()
{
base.OnInitialized();
Dispatcher.Dispatch(new CounterHubStartAction());
}
}
I subscribe to a few new Actions in the Toaster.razor
component:
protected override void OnInitialized()
{
...
SubscribeToAction<CounterHubReceiveCountAction>(ShowCountReceivedToast);
SubscribeToAction<CounterHubSendCountFailedAction>(ShowCountReceivedFailedToast);
SubscribeToAction<CounterHubSetConnectedAction>(ShowHubConnectedToast);
base.OnInitialized();
}
and
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:
<button class="btn btn-outline-warning"
@onclick="SendCount"
disabled="@(!CounterHubState.Value.Connected)">
Broadcast
</button>
and dispatch the Current Count when the button is clicked:
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.
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.
Using the default WithAutomaticReconnect
configuration, the clients will attempt to reconnect, and will re-enable the Broadcast button if successful.
This is what it looks like with multiple clients running at once:
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 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!
Top comments (15)
Very interesting 🤔I just started with SignalR and right now I only receive messages.
So I want to dip my toes in Fluxor using the SubscribeToAction that I find much more “clean” than using multiple CascadingValues
So what does a minimal hub look like? And how do I dispose the hub when my program stops?
Have created a gist instead off trying to attach a lot of code.
Looks good! I do like the ability to incorporate a SignalR hub into a Fluxor store. It opens up a lot of possibilities.
Thanks. Does Fluxor dispose Effect classes?
Really enjoyed these series! thanks!
Thanks has been looking for a real world example with 2-way data binding and API calls.
Maybe it's just me. But I think most users expect to see the initial state when the enter a webpage. People are also used to press F5 to refresh a page.
I know the weather forecast was an example on using API calsl. But in the real world people want a fresh weather forecast 😊
Any way, awesome job writing this go to guide on using Fluxor in Blazor
Great series. Thank you
Hi Eric,
I have a question on the WeatherStore example. Let's say I needed to wait for the LoadForecasts() to be completed and then do more async tasks afterword's that was dependent on the forecasts array. This would then need to be in the OnInitializedAsync override. How would I wait for the LoadForecasts to finish and then do the rest of the async tasks that I need to do? Right now, if I test it, once it hits the await Http.GetFromJson(...)... it then continues on with the rest of the code, even though the LoadForecasts effect has not yet completed. Below is the code I'm referring to in your series that I would like to put in the OnInitializedAsync override and put code after it once the LoadForecasts() has finished.
private WeatherForecast[] forecasts => WeatherState.Value.Forecasts;
private bool loading => WeatherState.Value.Loading;
I answered my own question I'm pretty sure. I need to SubscribeToAction and then load the rest of the data once that has been called. Below is some code that in my actual app where I'm waiting on UserSetUserResponseAction to be called then loading the rest of the component data. I need the CurrentUser to be able to finish the component data, so if it is not Initialized yet, then load the user data by dispatching the UserLoadUserResponseAction, else just use the UserStore.Value.CurrentUser.
I'd like to be able to just use UserStore.Value.CurrentUser instead of using another property that I'm setting, from the action.UserResponse, but the UserSetUserResponseAction callback is called before the reducer is which sets it, so UserStore.Value.CurrentUser is not yet set, when the rest of the code is run.
In the WeatherStore, you'll notice that the
LoadForecasts
EffectMethod dispatches theWeatherLoadForecastsSuccessAction
to indicate that the forecasts have been loaded. If I want to have some action happen after the forecasts have been loaded, I canSubscribeToAction<WeatherLoadForecastsSuccessAction>
and put whatever I need to do in there.It looks like that's what you're doing with the
SubscribeToAction<UserSetUserResponseAction>
so I think we're on the same page there.Thank you so much for this series of articles and taking the time to illustrate and explain everything along the way. This has been instrumental in helping me learn Blazor.
Thanks so much for the detailed and thorough tutorial series. I definitely learned so much more than the tutorial videos I've watched. Great job!
Thanks for this great walkthru, made it so much easier to make the switch from angular ngrx to fluxor.
Please could you share the code ?
Thank you
Code is at:
github.com/eric-king/BlazorWithFluxor
Thank you