Flux state is supposed to be immutable, and that state replaced only by pure functions, which should only take input from their parameters.
With this in mind, we need something that will enable us to access other sources of data such as web services, and then reduce the results into our state.
This tutorial will recreate the Fetch data
page in a standard Blazor app.
- Under the
Store
folder, create a new folder namedWeatherUseCase
. - Create a new state class to hold the state for this use case.
[FeatureState]
public class WeatherState
{
public bool IsLoading { get; }
public IEnumerable<WeatherForecast> Forecasts { get; }
private WeatherState() { }
public WeatherState(bool isLoading, IEnumerable<WeatherForecast> forecasts)
{
IsLoading = isLoading;
Forecasts = forecasts ?? Array.Empty<WeatherForecast>();
}
}
This state holds a property indicating whether or not the data is currently being retrieved from
the server, and an enumerable holding zero to many WeatherForecast
objects.
- Find the
Pages
folder and add a new file namedFetchData.razor.cs
- Mark the class
partial
.
Next we need to inject the WeatherState
into our component
public partial class FetchData
{
[Inject]
private IState<WeatherState> WeatherState { get; set; }
}
- Edit
FetchData.razor
and make the page descend fromFluxorComponent
.
@inherits Fluxor.Blazor.Web.Components.FluxorComponent
- Change the mark-up so it uses our
IsLoading
state to determine if data is being retrieved from the server or not.
Change
@if (forecasts == null)
to
@if (WeatherState.Value.IsLoading)
- Change the mark-up so it uses our
Forecasts
state.
Change
@foreach (var forecast in forecasts)
to
@foreach (var forecast in WeatherState.Value.Forecasts)
- Remove
@inject WeatherForecastService ForecastService
- In the
Store
folder, create an empty classFetchDataAction
. - In the
Store\WeatherFeature
folder, create a staticReducers
class, which will setIsLoading
to true when ourFetchDataAction
action is dispatched.
public static class Reducers
{
[ReducerMethod]
public static WeatherState ReduceFetchDataAction(WeatherState state, FetchDataAction action) =>
new WeatherState(
isLoading: true,
forecasts: null);
}
Alternatively, because we aren't using any values from the FetchDataAction action
we
can declare our reducer method without that parameter, like so:
public static class Reducers
{
[ReducerMethod(typeof(FetchDataAction))]
public static WeatherState ReduceFetchDataAction(WeatherState state) =>
new WeatherState(
isLoading: true,
forecasts: null);
}
- In
Fetchdata.razor.cs
injectIDispatcher
and dispatch our action from theOnInitialized
lifecycle method. The code-behind class should now look like this
public partial class FetchData
{
[Inject]
private IState<WeatherState> WeatherState { get; set; }
[Inject]
private IDispatcher Dispatcher { get; set; }
protected override void OnInitialized()
{
base.OnInitialized();
Dispatcher.Dispatch(new FetchDataAction());
}
}
Effect handlers cannot (and should not) affect state directly. They are triggered when the action they are interested in is dispatched through the store, and as a response they can dispatch new actions.
Effect handlers can be written in one of three ways.
- As with
[ReducerMethod]
, it is possible to use[EffectMethod]
without the action parameter being needed in the method signature.
[EffectMethod(typeof(FetchDataAction))]
public async Task HandleFetchDataAction(IDispatcher dispatcher)
{
var forecasts = await WeatherForecastService.GetForecastAsync(DateTime.Now);
dispatcher.Dispatch(new FetchDataResultAction(forecasts));
}
- By decorating instance or static methods with
[EffectMethod]
. The name of the class and the method are unimportant.
public class Effects
{
private readonly IWeatherForecastService WeatherForecastService;
public Effects(IWeatherForecastService weatherForecastService)
{
WeatherForecastService = weatherForecastService;
}
[EffectMethod]
public async Task HandleFetchDataAction(FetchDataAction action, IDispatcher dispatcher)
{
var forecasts = await WeatherForecastService.GetForecastAsync(DateTime.Now);
dispatcher.Dispatch(new FetchDataResultAction(forecasts));
}
}
- By descending from the
Effect<TAction>
class. The name of the class is unimportant, andTAction
identifies the type of action that should trigger thisEffect
.
public class FetchDataActionEffect : Effect<FetchDataAction>
{
private readonly IWeatherForecastService WeatherForecastService;
public FetchDataActionEffect(IWeatherForecastService weatherForecastService)
{
WeatherForecastService = weatherForecastService;
}
public override async Task HandleAsync(FetchDataAction action, IDispatcher dispatcher)
{
var forecasts = await WeatherForecastService.GetForecastAsync(DateTime.Now);
dispatcher.Dispatch(new FetchDataResultAction(forecasts));
}
}
These approaches work equally well, which you choose is an organisational choice. But keep in mind the following
- An
[EffectMethod]
can be declared either as static or instance. - If declared as an instance method, then an instance of the owning class will be created.
- Instance methods' dependencies will be injected.
- Only once instance of each owning class will be created, this means that multiple
[EffectMethod]
s can share property values (i.e. a CancellationToken).
I recommend you use approach 1 (static methods) when you do not need to access values in the action object,
otherwise use approach 2 (instance methods). Approach 3 (Effect<T>
descendant) is not
recommended due to the amount of code involved.
Important: Effects instances are created once per store instance and share a lifetime with the store.
This means that service instances injected into effects also share a lifetime with the store which, for long-lived scopes, means they will live for the life of the user's session. For example, Blazor apps keep one long-lived injection scope per browser window.
When injecting HttpClient
and you anticipate a possible DNS change you could consider instead using IHttpClientFactory
and requesting a HttpClient
per execution of the effect code. If you need a new instance of a service or each execution (e.g. a UnitOfWork
or DbContext
) you could consider using IServiceScopeFactory
to create a new instance of those services.
- In the
Store
folder, create a new classFetchDataResultAction
, which will hold the results of the call to the server so they can be "reduced" into our application state.
public class FetchDataResultAction
{
public IEnumerable<WeatherForecast> Forecasts { get; }
public FetchDataResultAction(IEnumerable<WeatherForecast> forecasts)
{
Forecasts = forecasts;
}
}
This is the action that is dispatched by our Effect
earlier, after it has retrieved the data from
the server via an HTTP request.
- Edit the
Reducers.cs
class and add a new[ReducerMethod]
to reduce the contents of this result action into state.
[ReducerMethod]
public static WeatherState ReduceFetchDataResultAction(
WeatherState state, FetchDataResultAction action) =>
new WeatherState(
isLoading: false,
forecasts: action.Forecasts);
This reducer simply sets the IsLoading
state back to false, and sets the Forecasts
state to the
values in the action that was dispatched by our effect.