DEV Community

Eric King
Eric King

Posted on • Updated on

Advanced Blazor State Management Using Fluxor, part 3 - Effects

This is the third 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.

What about Effects?

You may have noticed that I've mentioned a few times that Fluxor Stores are composed of 5 pieces: State, Feature, Actions, Reducers, and Effects. But so far we've only used 4 of those pieces. We haven't touched Effects yet. Why not?

An Effect in Fluxor is used when a dispatched Action needs to access resources outside of the Store in a way that a "pure" Reducer method cannot. A common example is making an HTTP call to an API. A Reducer method only has access to the current State and the Action it's subscribing to.

An Effect method can access outside resources, and in turn dispatch Actions itself that will be processed by Reducers to emit new State.

A class that contains Effect methods is an instance class (non-static) that can, through its constructor, have resources injected into it on instantiation. Anything that can be injected into a component using the standard dependency injection mechanism in Blazor can be injected into an Effect class instance.

Weather Forecasts

The "Fetch Data" component in the Blazor WebAssembly Hosted template that we're using is a component that, since it makes an API call to retrieve Weather Forecast data, can make use of an Effect method, so that's what we're going to do.

Consider the following scenario:

The Weather Forecasts should only be automatically retrieved the first time the page is loaded. On subsequent views, the already-retrieved forecasts should be displayed, not new ones.

New Weather Forecasts should be retrieved only when clicking a "Refresh Forecasts" button on the page.

Currently, the page will load new forecasts every time the page is viewed:

Alt Text

This is because the WeatherForecast[] is populated whenever the component is initialized, which is every time it's rendered.

@code {
    private WeatherForecast[] forecasts;

    protected override async Task OnInitializedAsync()
    {
        forecasts = await Http.GetFromJsonAsync<WeatherForecast[]>("WeatherForecast");
    }
}

Enter fullscreen mode Exit fullscreen mode

What we really want is some state to hold on to the Forecasts and redisplay when the page is loaded, and a way to track whether the State has been initialized. Perhaps something like this:

public record WeatherState 
{
    public bool Initialized { get; init; }
    public bool Loading { get; init; }
    public WeatherForecast[] Forecasts { get; init; }
}
Enter fullscreen mode Exit fullscreen mode

So lets turn this component into a Fluxor feature.

Step one, make a place for the Weather feature. Create a \Features\Weather folder, and inside that create a Pages folder and a Store folder. Move the FetchData.razor to the newly created Pages folder. In the Store folder create a WeatherStore.cs file.

In the WeatherStore goes the 5 pieces of a Fluxor Store: State, Feature, Actions, Reducers, Effects.

We have a WeatherState, as shown above.

The Feature:

public class WeatherFeature : Feature<WeatherState>
{
    public override string GetName() => "Weather";

    protected override WeatherState GetInitialState()
    {
        return new WeatherState
        {
            Initialized = false,
            Loading = false,
            Forecasts = Array.Empty<WeatherForecast>()
        };
    }
}
Enter fullscreen mode Exit fullscreen mode

The Actions:

public class WeatherSetInitializedAction { }

public class WeatherSetForecastsAction
{
    public WeatherForecast[] Forecasts { get; }

    public WeatherSetForecastsAction(WeatherForecast[] forecasts)
    {
        Forecasts = forecasts;
    }
}

public class WeatherSetLoadingAction
{
    public bool Loading { get; }

    public WeatherSetLoadingAction(bool loading)
    {
        Loading = loading;
    }
}
Enter fullscreen mode Exit fullscreen mode

The Reducers:

public static class WeatherReducers 
{
    [ReducerMethod]
    public static WeatherState OnSetForecasts(WeatherState state, WeatherSetForecastsAction action) 
    {
        return state with 
        {
            Forecasts = action.Forecasts
        };
    }

    [ReducerMethod]
    public static WeatherState OnSetLoading(WeatherState state, WeatherSetLoadingAction action)
    {
        return state with
        {
            Loading = action.Loading
        };
    }

    [ReducerMethod(typeof(WeatherSetInitializedAction))]
    public static WeatherState OnSetInitialized(WeatherState state)
    {
        return state with
        {
            Initialized = true
        };
    }
}
Enter fullscreen mode Exit fullscreen mode

Which will allow us to change the FetchData.razor file like by adding the Fluxor requirements up top:

@inherits FluxorComponent

@using BlazorWithFluxor.Shared
@using BlazorWithFluxor.Client.Features.Weather.Store

@inject IDispatcher Dispatcher
@inject IState<WeatherState> WeatherState
@inject HttpClient Http
Enter fullscreen mode Exit fullscreen mode

Change the @code block to something like:

@code {

    private WeatherForecast[] forecasts => WeatherState.Value.Forecasts;
    private bool loading => WeatherState.Value.Loading;

    protected override async Task OnInitializedAsync()
    {
        if (WeatherState.Value.Initialized == false)
        {
            await LoadForecasts();
            Dispatcher.Dispatch(new WeatherSetInitializedAction());
        }
        await base.OnInitializedAsync();
    }

    private async Task LoadForecasts()
    {
        Dispatcher.Dispatch(new WeatherSetLoadingAction(true));
        var forecasts = await Http.GetFromJsonAsync<WeatherForecast[]>("WeatherForecast");
        Dispatcher.Dispatch(new WeatherSetForecastsAction(forecasts));
        Dispatcher.Dispatch(new WeatherSetLoadingAction(false));
    }

}
Enter fullscreen mode Exit fullscreen mode

Exchange this awkward null check in the markup

@if (forecasts == null)
{
    <p><em>Loading...</em></p>
}
Enter fullscreen mode Exit fullscreen mode

with something more meaningful:

@if (_loading)
{
    <p><em>Loading...</em></p>
}
Enter fullscreen mode Exit fullscreen mode

And add a button to trigger a reload of the forecasts:

</table>
<br />
<button class="btn btn-outline-info" @onclick="LoadForecasts">Refresh Forecasts</button>
Enter fullscreen mode Exit fullscreen mode

Build, run, and it works:

Alt Text

However, this part is still a bit awkward:

private async Task LoadForecasts()
{
    Dispatcher.Dispatch(new WeatherSetLoadingAction(true));
    var forecasts = await Http.GetFromJsonAsync<WeatherForecast[]>("WeatherForecast");
    Dispatcher.Dispatch(new WeatherSetForecastsAction(forecasts));
    Dispatcher.Dispatch(new WeatherSetLoadingAction(false));
}
Enter fullscreen mode Exit fullscreen mode

There is a lot of state-manipulation going on here, which seems like it would be more appropriately placed in the WeatherStore. What we really want is something more like this:

private void LoadForecasts()
{
    Dispatcher.Dispatch(new WeatherLoadForecastsAction());
}
Enter fullscreen mode Exit fullscreen mode

The handler of that Action, in the WeatherStore, would take care care of all of the State manipulation and leave the page to react to the changing State (see what I did there 😉) rather than drive it.

This is where Effect methods come into play. Since a Reducer cannot call out to the API, we need an Effect method to do it.

So, back to the WeatherStore we go, to add a new Action:

public class WeatherLoadForecastsAction { }
Enter fullscreen mode Exit fullscreen mode

And a WeatherEffects class with one EffectMethod:

public class WeatherEffects 
{
    private readonly HttpClient Http;

    public WeatherEffects(HttpClient http)
    {
        Http = http;
    }

   [EffectMethod(typeof(WeatherLoadForecastsAction))]
    public async Task LoadForecasts(IDispatcher dispatcher) 
    {
        dispatcher.Dispatch(new WeatherSetLoadingAction(true));
        var forecasts = await Http.GetFromJsonAsync<WeatherForecast[]>("WeatherForecast");
        dispatcher.Dispatch(new WeatherSetForecastsAction(forecasts));
        dispatcher.Dispatch(new WeatherSetLoadingAction(false));
    }
}
Enter fullscreen mode Exit fullscreen mode

It is similar to the WeatherReducers class, but with one big and obvious difference: this one has a constructor into which we can inject dependencies, and which we can use in each EffectMethod.

Like a [ReducerMethod], an [EffectMethod] can be declared with the Action Type in the attribute (in the case of Actions with no payload), or as a method parameter:

[EffectMethod]
public async Task LoadForecasts(WeatherLoadForecastsAction action, IDispatcher dispatcher) 
{
    // code
}
Enter fullscreen mode Exit fullscreen mode

In this case there is no payload so we declare the Action in the attribute.

Now we can go back to the FetchData.razor file, remove the @inject HttpClient Http since we no longer need it, and the page's @code block now looks like:

@code {

    private WeatherForecast[] forecasts => WeatherState.Value.Forecasts;
    private bool loading => WeatherState.Value.Loading;

    protected override void OnInitialized()
    {
        if (WeatherState.Value.Initialized == false)
        {
            LoadForecasts();
            Dispatcher.Dispatch(new WeatherSetInitializedAction());
        }
        base.OnInitialized();
    }

    private void LoadForecasts()
    {
        Dispatcher.Dispatch(new WeatherLoadForecastsAction());
    }

}
Enter fullscreen mode Exit fullscreen mode

Build, run, and we still should see the same behavior as above. The forecasts load once on component initialize, the component maintains its state between views, and reloads the forecasts when the 'Refresh Forecasts' button is clicked.

As for the WeatherEffects class... What did we gain besides moving some code around? In the next part of this series, we'll explore how having the "Load Forecasts" functionality in the WeatherLoadForecastsAction opens up some opportunities for more complex interactions between application components.

After all of these steps, the solution should look like this branch of the demo repository.

Please leave a comment if you find this helpful, have a suggestion, or want to ask a question.

Happy coding!


Edit: Peter, in his comment below, suggests an adjustment to the Store that simplifies a few things. Here is what the Actions/Reducers/Effects would look like after the changes:

public static class WeatherReducers 
{
    [ReducerMethod]
    public static WeatherState OnSetForecasts(WeatherState state, WeatherSetForecastsAction action) 
    {
        return state with 
        {
            Forecasts = action.Forecasts,
            Loading = false
        };
    }

    [ReducerMethod(typeof(WeatherSetInitializedAction))]
    public static WeatherState OnSetInitialized(WeatherState state)
    {
        return state with
        {
            Initialized = true
        };
    }

    [ReducerMethod(typeof(WeatherLoadForecastsAction))]
    public static WeatherState OnLoadForecasts(WeatherState state)
    {
        return state with
        {
            Loading = true
        };
    }
}

public class WeatherEffects 
{
    private readonly HttpClient Http;
    private readonly IState<CounterState> CounterState;

    public WeatherEffects(HttpClient http, IState<CounterState> counterState)
    {
        Http = http;
        CounterState = counterState;
    }

    [EffectMethod(typeof(WeatherLoadForecastsAction))]
    public async Task LoadForecasts(IDispatcher dispatcher) 
    {
        var forecasts = await Http.GetFromJsonAsync<WeatherForecast[]>("WeatherForecast");
        dispatcher.Dispatch(new WeatherSetForecastsAction(forecasts));
    }
}

#region WeatherActions
public class WeatherSetInitializedAction { }
public class WeatherLoadForecastsAction { }
public class WeatherSetForecastsAction
{
    public WeatherForecast[] Forecasts { get; }

    public WeatherSetForecastsAction(WeatherForecast[] forecasts)
    {
        Forecasts = forecasts;
    }
}
#endregion

Enter fullscreen mode Exit fullscreen mode

This has the effect of simplifying the LoadForecasts EffectMethod, and moving the update of the Loading state to reducers that subscribe directly to WeatherLoadForecastsAction and WeatherSetForecastsAction, removing the need for the Effect to dispatch extra Actions.

Top comments (7)

Collapse
 
momboman profile image
MomboMan

Hey Eric,

In this page's code nor the final version in Github, I can't find where the WeatherEffects 'Http' parameter gets set, i.e. who is calling this:

        public WeatherEffects(HttpClient http, IState<CounterState> counterState, ILocalStorageService localStorageService)
        {
            Http = http;
            CounterState = counterState;
            _localStorageService = localStorageService;
        }
Enter fullscreen mode Exit fullscreen mode
Collapse
 
mr_eking profile image
Eric King

The constructor for the effect class is called by the Fluxor library itself. The arguments are fulfilled by the standard Dependency Injection mechanism, by adding services in the Client/Program.cs class. Look in there and you'll see the HttpClient added explicitly, while the rest of the services are provided by the AddFluxor and AddBlazoredLocalStorage methods.

Collapse
 
momboman profile image
MomboMan

Urgh. Too subtle for this old guy to catch.

Thanks

Collapse
 
mrpmorris profile image
Peter Morris

To simplify, you can eliminate the WeatherSetLoadingAction and just set the loading state to true in the reducer for WeatherLoadForecastsAction, and false in the reducer for WeatherSetForecastsAction

Collapse
 
mr_eking profile image
Eric King

Nice, thank you.

And thank you for your work on Fluxor.

Some comments may only be visible to logged-in visitors. Sign in to view all comments.