DEV Community

Eric King
Eric King

Posted on • Updated on

 

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

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.

Top comments (5)

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

Oh I didn't know that. Even better. ๐Ÿ˜Š

Collapse
 
bvanderpoel profile image
Bart van der Poel • Edited

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.

Collapse
 
mr_eking profile image
Eric King

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

if (Middlewares.All(x => x.MayDispatchAction(nextActionToProcess)))
{
    ExecuteMiddlewareBeforeDispatch(nextActionToProcess);

    // Notify all features of this action
    foreach (var featureInstance in FeaturesByName.Values)
        featureInstance.ReceiveDispatchNotificationFromStore(nextActionToProcess);

    ActionSubscriber?.Notify(nextActionToProcess);
    ExecuteMiddlewareAfterDispatch(nextActionToProcess);
    TriggerEffects(nextActionToProcess);
}
Enter fullscreen mode Exit fullscreen mode

Here's the code in the Feature class that's invoking the reducers.

public virtual void ReceiveDispatchNotificationFromStore(object action)
{
    if (action is null)
        throw new ArgumentNullException(nameof(action));

    IEnumerable<IReducer<TState>> applicableReducers = Reducers.Where(x => x.ShouldReduceStateForAction(action));
    TState newState = State;
    foreach (IReducer<TState> currentReducer in applicableReducers)
    {
        newState = currentReducer.Reduce(newState, action);
    }
    State = newState;
}
Enter fullscreen mode Exit fullscreen mode
Collapse
 
bvanderpoel profile image
Bart van der Poel

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:

Image description

Image description

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.

Regex for lazy developers

regex for lazy devs

You know who you are. Sorry for the callout ๐Ÿ˜†