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:
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");
}
}
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; }
}
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>()
};
}
}
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;
}
}
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
};
}
}
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
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));
}
}
Exchange this awkward null check in the markup
@if (forecasts == null)
{
<p><em>Loading...</em></p>
}
with something more meaningful:
@if (_loading)
{
<p><em>Loading...</em></p>
}
And add a button to trigger a reload of the forecasts:
</table>
<br />
<button class="btn btn-outline-info" @onclick="LoadForecasts">Refresh Forecasts</button>
Build, run, and it works:
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));
}
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());
}
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 { }
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));
}
}
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
}
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());
}
}
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
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)
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:
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.
Urgh. Too subtle for this old guy to catch.
Thanks
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
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.