DEV Community

loading...

Advanced Blazor State Management Using Fluxor, part 4 - Interactions between Features

Eric King
Updated on ・5 min read

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());
        }
    }
}

Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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());
    }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

In the @code block, let's add a variable for the css class:

private string WeatherItemClass => WeatherState.Value.Loading ? "font-weight-bold" : null;
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

Now, if we build and run the application, we can see that the Weather nav menu link displays bold while the forecasts are loading:

Alt Text

And that the forecast loading is triggered by every 10th increment of the Counter:

Alt Text

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
Enter fullscreen mode Exit fullscreen mode

In Program.cs we need to add using Blazored.Toast; and this line:

builder.Services.AddBlazoredToast();
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Add the css reference to the wwwroot\index.html file:

<link href="_content/Blazored.Toast/blazored-toast.min.css" rel="stylesheet" />
Enter fullscreen mode Exit fullscreen mode

And finally we'll configure the Toasts component by adding this to the MainLayout.razor file:

<BlazoredToasts Position="ToastPosition.TopRight" Timeout="3"/>
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Add a function in the @code block to show the Toast message:

private void ShowWeatherToast()
{
    toastService.ShowInfo("Weather Forecasts have been updated!");
}
Enter fullscreen mode Exit fullscreen mode

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();
}
Enter fullscreen mode Exit fullscreen mode

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!");
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

Build, run, and we can see the IActionSubscriber doing its work:

Alt Text

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

Notice that SubscribeToAction is called directly, and with no need for the this parameter.

Discussion (2)

Collapse
mrpmorris profile image
Peter Morris

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 :)

Collapse
mr_eking profile image
Eric King Author

Oh I didn't know that. Even better. 😊