I'm a massive fan of the Redux pattern. I would say one thing, though, it's not everyone's cup of tea. It adds extra boilerplate code and may look overwhelming at first. But you get used to it and start to appreciate its power once your application grows to be a giant where state management becomes a nightmare.
Talking of sidekicks, React has Redux; Angular has NGRX; what does Blazor have? I'm really into this library called Fluxor recently. You can carry over your existing knowledge of any Redux
based state management library over here, and you are good to go.
You start by adding the following packages to your Blazor Server/WASM application,
dotnet add package Fluxor.Blazor.Web
dotnet add package Fluxor.Blazor.Web.ReduxDevTools
Register Fluxor
services with the default IoC
container. You would do that in the Program.cs
file,
public static async Task Main(string[] args)
{
var builder = WebAssemblyHostBuilder.CreateDefault(args);
/* Code removed for brevity */
builder.Services.AddFluxor(opt =>
{
opt.ScanAssemblies(typeof(Program).Assembly);
opt.UseRouting();
opt.UseReduxDevTools();
});
await builder.Build().RunAsync();
}
Inside index.html
, add the script reference for Fluxor.Blazor.Web
,
<!DOCTYPE html>
<html>
<head>
<!-- Code removed for brevity -->
</head>
<body>
<!-- Code removed for brevity -->
<script src="_framework/blazor.webassembly.js"></script>
<script src="_content/Fluxor.Blazor.Web/scripts/index.js"></script>
</body>
</html>
Store:
As the name suggests, a store persists in an application state tree, i.e., the build-out of different feature states. The Store
has to be initialized when the application first kicks off. Hence, it is best to put out the Store
initialization logic in the App
component,
<Fluxor.Blazor.Web.StoreInitializer />
<Router AppAssembly="@typeof(Program).Assembly">
<!-- Code remve for brevity -->
</Router>
Feature:
Feature
is an area that provides a new piece of state for the Store
. To declare a feature, you would want to extend the abstract Feature<T>
class available in Fluxor
. Feature<T>
takes a state type. The following feature class can be used to name a feature and an initial state for the CounterState
type,
public class Feature : Feature<CounterState>
{
public override string GetName()
{
throw new NotImplementedException();
}
protected override CounterState GetInitialState()
{
throw new NotImplementedException();
}
}
The state contains nothing but some properties. A state should be immutable in nature. You should never mutate a state directly, rather return a new state by changing its different properties. Hence, we can go for a record
instead of a class
,
public record CounterState(int Count);
public class Feature : Feature<CounterState>
{
public override string GetName() => "Counter";
protected override CounterState GetInitialState() => new(0);
}
CounterState
contains a init
only property Count
and it is initialized with the value 0
.
Action:
A state should change based on different actions dispatched on it. An action may or may not have arguments. For example, an IncrementCounterAction
does nothing but increment the Count
property by 1
; but if you want to change the increment value to something more dynamic, use arguments.
public record IncrementCounterAction();
public record IncrementCounterByAction(int IncrementBy);
Reducer:
A reducer is a pure function that takes the current state and action being dispatched upon it. Depending on the action type, it produces a new state and returns it.
public class Reducers
{
[ReducerMethod]
public static CounterState ReduceIncrementCounterAction(CounterState state, IncrementCounterAction action) =>
state with { Count = state.Count + 1 };
[ReducerMethod]
public static CounterState ReduceIncrementCounterByAction(CounterState state, IncrementCounterByAction action) =>
state with { Count = state.Count + action.IncrementBy };
}
Effect:
Effects are used to handle side effects in your application. A side effect can be anything from a long-running task to an HTTP call to service. In these cases, state changes are not instantaneous. Effects perform tasks, which are synchronous/asynchronous, and dispatch different actions based on the outcome. The following uses the HttpClient
to asynchronously get the content of the sample-data/weather.json
file.
public class Effects
{
private readonly HttpClient _httpClient;
public Effects(HttpClient httpClient)
{
_httpClient = httpClient;
}
[EffectMethod]
public async Task HandleAsync(FetchDataAction action, IDispatcher dispatcher)
{
try
{
var forecasts = await _httpClient.GetFromJsonAsync<WeatherForecast[]>("sample-data/weather.json");
dispatcher.Dispatch(new FetchDataSuccessAction(forecasts));
}
catch (Exception ex)
{
dispatcher.Dispatch(new FetchDataErrorAction(ex.Message));
}
}
}
Check the FetchDataUseCase to get a better understanding of the related state, actions, reducers and effects.
Accessing State:
Feature states can be injected into a razor component using the IState<T>
interface. The following shows how to inject the Counter
state into the Counter.razor
component,
@page "/counter"
@using Fluxord
@inject IState<CounterState> _counterState
<h1>Counter</h1>
<p>Current count: @_counterState.Value.Count</p>
You use the Value
property to get access to different properties of the injected state
Dispatching Action:
To dispatch an action on the store depending on a UI event, inject the IDispatcher
service and use the Dispatch
the method passing the type of action.
@page "/counter"
@using Fluxor
@using FlashCardsWasm.Store.CounterUseCase
@inject IState<CounterState> _counterState
@inject IDispatcher _dispatcher
<h1>Counter</h1>
<p>Current count: @_counterState.Value.Count</p>
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
@code {
private void IncrementCount()
{
_dispatcher.Dispatch(new IncrementCounterAction());
}
}
A similar approach is taken FetchData
component. Although one thing to notice here is the inheritance of the component from FluxorComponent
. Dispatching of the FetchDataSuccessAction
and FetchDataErrorAction
is happening from within the Effects
. The UI has to be notified i.e. call StateHasChanged
when these actions are dispatched. Using the FluxorComponent
takes care of that.
@page "/fetchdata"
@using Fluxor
@using FlashCardsWasm.Store.FetchDataUseCase
@using static FlashCardsWasm.Store.FetchDataUseCase.Actions
@inherits Fluxor.Blazor.Web.Components.FluxorComponent
@inject IState<FetchDataState> _fetchDataState
@if (_fetchDataState.Value.Error != null)
{
<MudAlert Severity="Severity.Error">@_fetchDataState.Value.Error</MudAlert>
}
else
{
<MudGrid Class="mt-4">
<MudItem xs="12" sm="12" md="12">
<MudText Typo="Typo.h3">Weather forecast</MudText>
<MudText Typo="Typo.subtitle1">This component demonstrates fetching data from a service.</MudText>
</MudItem>
<MudItem xs="12" sm="12" md="12">
<MudTable Items="@_fetchDataState.Value.Forecasts" Hover="true" Breakpoint="Breakpoint.Md" Loading="@_fetchDataState.Value.IsLoading"
LoadingProgressColor="Color.Info">
<HeaderContent>
<MudTh>Date</MudTh>
<MudTh>Temp. (C)</MudTh>
<MudTh>Temp. (F)</MudTh>
<MudTh>Summary</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd DataLabel="Date">@context.Date</MudTd>
<MudTd DataLabel="TempC">@context.TemperatureC</MudTd>
<MudTd DataLabel="TempF">@context.TemperatureF</MudTd>
<MudTd DataLabel="Summary">@context.Summary</MudTd>
</RowTemplate>
</MudTable>
</MudItem>
</MudGrid>
}
@code {
[Inject]
public IDispatcher Dispatcher { get; set; }
protected override void OnInitialized()
{
Dispatcher.Dispatch(new FetchDataAction());
base.OnInitialized();
}
}
Redux DevTools
To tinker around with the application state, you should have the Redux DevTools extension installed in your preferred browser. Open up the Developer Console
of your browser and go to the Redux
tab. You will see what actions are dispatched and how your application state is modified.
Repository Link:
Part II - https://github.com/fiyazbinhasan/FlashCardsWasm/tree/Part-II-State-Management-With-Fluxor
Important Links:

Comments