This is the fourth 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.
So far in this series everything we've explored has been limited to stateful interaction within a single Feature and its Store.
But what about interaction between Features? That's possible too, and here's one way to do it.
Imagine a scenario where we want to trigger an action in one Feature based on the state of another Feature. For instance:
Refresh the Weather Forecasts on every 10th Increment of the Counter.
One approach might be to put code in the Counter.razor
page that inspects the CounterState
and dispatches a WeatherLoadForecastsAction
at the appropriate time.
@code {
private void IncrementCount()
{
Dispatcher.Dispatch(new CounterIncrementAction());
// every tenth increment
if (CounterState.Value.CurrentCount % 10 == 0)
{
Dispatcher.Dispatch(new WeatherLoadForecastsAction());
}
}
}
But I don't prefer that approach because it puts application logic for the Weather Feature in the Counter Feature's UI component.
We could change the CounterStore so that the CounterIncrementAction
is handled by an Effect
rather than a Reducer
, so that the Effect
can dispatch a WeatherLoadForecastsAction
. I don't prefer this approach either, because it complicates the Counter Feature unnecessarily with Weather Feature concerns.
This functionality really belongs in the WeatherStore. The WeatherStore should be able to react to a dispatch of CounterIncrementAction
and respond appropriately.
So how would we do that?
Since we can inject dependencies into Effects classes, we can adjust the WeatherEffects
class to have the CounterState injected:
private readonly HttpClient Http;
private readonly IState<CounterState> CounterState;
public WeatherEffects(HttpClient http, IState<CounterState> counterState)
{
Http = http;
CounterState = counterState;
}
Then we can add an Effect method to handle the dispatched action:
[EffectMethod(typeof(CounterIncrementAction))]
public async Task LoadForecastsOnIncrement(IDispatcher dispatcher)
{
// every tenth increment
if (CounterState.Value.CurrentCount % 10 == 0)
{
dispatcher.Dispatch(new WeatherLoadForecastsAction());
}
}
This Effect method in turn triggers the LoadForecasts
Effect method we created previously (via the WeatherLoadForecastsAction
), and the forecasts are updated even though we're not interacting directly with the Weather page in any way.
If we run the app now, it's not apparent when the forecasts are updated unless we're looking at the weather forecasts page. So, let's change the NavMenu so that the Fetch Data menu item is displayed in bold text when the forecasts are loading.
In the NavMenu.razor
file add:
@using BlazorWithFluxor.Client.Features.Weather.Store
@inject IState<WeatherState> WeatherState
In the @code
block, let's add a variable for the css class:
private string WeatherItemClass => WeatherState.Value.Loading ? "font-weight-bold" : null;
And change the NavMenu label for the weather page to wrap it in a <span>
so the css class can be applied:
<span class="@WeatherItemClass">Weather</span>
Now, if we build and run the application, we can see that the Weather nav menu link displays bold while the forecasts are loading:
And that the forecast loading is triggered by every 10th increment of the Counter:
All this using just the functionality provided by Actions
, Reducers
, and Effects
.
IActionSubscriber
Now that we're triggering the updates of the weather forecasts based on activity in other application features, our users have asked for us to provide better notification than just turning a menu link bold. They have handed us this requirement:
When weather forecasts are updated, show a temporary popup message in the top right of the screen informing that the forecasts have been updated.
In this scenario we don't need to update the state of the application, we just need to trigger a notification. In scenarios like this, we can use a Fluxor feature called an IActionSubscriber. This allows a razor component to react to a dispatched Action without having to go through the Store to do it.
We're going to use the IActionSubscriber
and Blazored.Toast to accommodate this new feature request.
First, let's install the Blazored.Toast
NuGet package:
Install-Package Blazored.Toast
In Program.cs
we need to add using Blazored.Toast;
and this line:
builder.Services.AddBlazoredToast();
For convenience, we'll add a few using statements to the _Imports.razor
file:
@using Blazored.Toast
@using Blazored.Toast.Configuration
@using Blazored.Toast.Services
Add the css reference to the wwwroot\index.html
file:
<link href="_content/Blazored.Toast/blazored-toast.min.css" rel="stylesheet" />
And finally we'll configure the Toasts component by adding this to the MainLayout.razor
file:
<BlazoredToasts Position="ToastPosition.TopRight" Timeout="3"/>
And with that, we're ready to trigger the Toasts. Since the NavMenu component is always loaded (in this template), I'm going to put the IActionSubscriber
and the IToastService
in the NavMenu.razor
component.
At the top of NavMenu.razor
inject the services:
@inject IToastService toastService
@inject IActionSubscriber ActionSubscriber
Add a function in the @code
block to show the Toast message:
private void ShowWeatherToast()
{
toastService.ShowInfo("Weather Forecasts have been updated!");
}
And let's use the ActionSubscriber
to subscribe to the WeatherSetForecastsAction
and invoke the ShowWeatherToast
method:
protected override void OnInitialized()
{
ActionSubscriber.SubscribeToAction<WeatherSetForecastsAction>(this, (action) => ShowWeatherToast());
base.OnInitialized();
}
This line of code will tell the Fluxor Dispatcher that any time the WeatherSetForecastsAction
is dispatched, invoke the given Action. The first parameter this
represents the instance of the NavMenu component, and the second parameter represents an Action ("Action" in the .NET sense, not in the Flux Action sense) to invoke upon dispatch.
We can actually simplify this a little bit if we change the void ShowWeatherToasts()
method to void ShowWeatherToasts(WeatherSetForecastsAction action)
, since that effectively turns it into an Action<WeatherSetForecastsAction>
. The simplified version would look like:
protected override void OnInitialized()
{
ActionSubscriber.SubscribeToAction<WeatherSetForecastsAction>(this, ShowWeatherToast);
base.OnInitialized();
}
private void ShowWeatherToast(WeatherSetForecastsAction action)
{
toastService.ShowInfo("Weather Forecasts have been updated!");
}
Since the subscription is created for the instance of the component (the this
parameter), it's important to remove the subscription when the instance is disposed. So any time you SubscribeToAction
, remember to unsubscribe when it's appropriate. We'll do so in the component's Dispose
method:
protected override void Dispose(bool disposing)
{
ActionSubscriber.UnsubscribeFromAllActions(this);
base.Dispose(disposing);
}
The NavMenu.razor's @code
block should now look like:
@code {
private bool collapseNavMenu = true;
private string NavMenuCssClass => collapseNavMenu ? "collapse" : null;
private string WeatherItemClass => WeatherState.Value.Loading ? "font-weight-bold" : null;
protected override void OnInitialized()
{
ActionSubscriber.SubscribeToAction<WeatherSetForecastsAction>(this, ShowWeatherToast);
base.OnInitialized();
}
protected override void Dispose(bool disposing)
{
ActionSubscriber.UnsubscribeFromAllActions(this);
base.Dispose(disposing);
}
private void ShowWeatherToast(WeatherSetForecastsAction action)
{
toastService.ShowInfo("Weather Forecasts have been updated!");
}
private void ToggleNavMenu()
{
collapseNavMenu = !collapseNavMenu;
}
}
Build, run, and we can see the IActionSubscriber
doing its work:
In the next post in the series, we'll tackle the challenge of meshing the read-only nature of State in Flux with the two-way data binding of razor EditForms
.
Until then, happy coding!
Edit: Today I learned (from Peter's comment below) that you don't actually have to inject the IActionSubscriber into a component such as this, which
@inherits FluxorComponent
- the ActionSubscriber is already available and (better yet!) unsubscribing will happen automatically. No need to write that code.
With that in mind, here is an updated NavMenu@code
block after removing the@inject IActionSubscriber ActionSubscriber
line at the top of the file:
@code {
private bool collapseNavMenu = true;
private string NavMenuCssClass => collapseNavMenu ? "collapse" : null;
private string WeatherItemClass => WeatherState.Value.Loading ? "font-weight-bold" : null;
protected override void OnInitialized()
{
SubscribeToAction<WeatherSetForecastsAction>(ShowWeatherToast);
base.OnInitialized();
}
private void ShowWeatherToast(WeatherSetForecastsAction action)
{
toastService.ShowInfo("Weather Forecasts have been updated!");
}
private void ToggleNavMenu()
{
collapseNavMenu = !collapseNavMenu;
}
}
Notice that SubscribeToAction
is called directly, and with no need for the this
parameter.
Top comments (5)
If your component descends from FluxorComponent then you can just use the ActionSubscriber property, no need to inject IActionSubscriber, and no need to unsubscribe either as it is done for you.
If injecting IActionSubscriber though (because it isn't a FluxorComponent) then this is absolutely the right way to do it.
It was a very good blog series :)
Oh I didn't know that. Even better. 😊
Since you have both a reducer and an effect being dispatched on CounterIncrementAction, and the weather seems to always be loaded when reaching 10, is it then true that Reducers are always called *before * effects in a synchronous manner?
I have some state that needs to update using a reducer, but then some "calculable" derived state to start updating after this value has been updated.
I believe so, yes. If you look in the Fluxor source for the Store class, you can see that when the store is dispatching an action, it first notifies each feature of the action to be processed (which does the reducing, updating State), and only afterwards does it trigger any effects (in the last line).
Here's the code in the Feature class that's invoking the reducers.
Thanks for the help! Though there is no possibility of a race condition with my current implementation, the sequence and intention is not so clear. Also, the derived state is in a sense redundant, and the store should be minimal.
I found the concept of "Selectors " in Redux (note that I have lot of back-end experience with numerical solvers, but hardly any front-end, so forgive me my lack of knowledge).
Did found something like a StateSelector in Fluxor, but no documentation on it, so I created something myself:
The idea is that you supply an IState and a pure method. On any change of the base-state, the derived state will be calculated based on the supplied pure method, and anyone using this property will be notified. This seem to work beautifully.
I currently use a factory method for instantiation (called in OnInitialized), though I think this might be improved by applying attributes to these pure methods in a static class as [FluxorSelectorMethod] similarly as [ReducerMethod], and automate the rest of it via injected services and (derived) states.