DEV Community

loading...

Advanced Blazor State Management Using Fluxor, part 3 - Effects

Eric King
・Updated on ・6 min read

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.

Discussion (5)

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 Author

Nice, thank you.

And thank you for your work on Fluxor.

Collapse
jnifm profile image
jn-ifm

To further simplify the OnSetInitialized action and reducer could be kicked out and the Initialized flag can be set true on the OnSetForecasts reducer?

Collapse
mr_eking profile image
Eric King Author

Yes, that's true. I think I had a reason for the separate Initialized pieces at one point, but the way it ended up it's not necessary, as you point out. Thanks!