Blazor Flash Cards - State Management with Fluxor



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();
}
Program.cs

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>
index.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>
App.razor

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();
    }
}
Feature.cs

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);
}
Feature.cs

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);
Actions.cs

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 };
}
Reducers.cs

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));
        }
    }
}
Effects.cs
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>
Counter.razor
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());
    }
}
Counter.razor

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();
    }
}
FetchData.razor

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.

Part II - https://github.com/fiyazbinhasan/FlashCardsWasm/tree/Part-II-State-Management-With-Fluxor

fiyazbinhasan/FlashCardsWasm
Contribute to fiyazbinhasan/FlashCardsWasm development by creating an account on GitHub.
mrpmorris/Fluxor
Fluxor is a zero boilerplate Flux/Redux library for Microsoft .NET and Blazor. - mrpmorris/Fluxor
GeekHour.AspNetCore.BlazorFluxor.Templates 1.0.1
Project templates Blazor and Fluxor.