DEV Community

loading...

Advanced Blazor State Management Using Fluxor, part 2 - Starting with Fluxor

Eric King
Updated on ・7 min read

This is the second 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. If you haven't read the previous posts, please start there.

Let's get to some code, shall we?

I'll be illustrating these examples using the Blazor WebAssembly, ASP.NET hosted template in Visual Studio 2019 Community. I'm a big Visual Studio, so that's my preferred IDE, but you can use Visual Studio Code instead if you prefer. Just execute dotnet new blazorwasm -ho in the command shell. The -ho indicates the "ASP.NET Core hosted" option. In Visual Studio 2019, this is what I'm using:

Alt Text

If you build and run at this point, you'll see an application with a Counter page that exhibits the state-losing behavior I noted in the previous blog post:

Counter component losing state after navigation

Installing Fluxor

Before we begin: The code for this blog series can be found on github as shown below. To check your progress for this step of the series, view the 002_Add_Fluxor_Libraries branch.

GitHub logo eric-king / BlazorWithFluxor

A demo of Blazor Wasm using Fluxor as application state management

To add Fluxor to the project and begin our state-management journey, first install the Fluxor.Blazor.Web Nuget package to the .Client project in the solution:

Install-Package Fluxor.Blazor.Web
Enter fullscreen mode Exit fullscreen mode

Alter the Program.cs file to include this line, just above the line that begins "await builder.Build()"

builder.Services.AddFluxor(o => o.ScanAssemblies(typeof(Program).Assembly));
Enter fullscreen mode Exit fullscreen mode

and the accompanying using statement at the top:

using Fluxor;
Enter fullscreen mode Exit fullscreen mode

For convenience, add a couple of Fluxor using statements to the _Imports.razor file:

@using Fluxor
@using Fluxor.Blazor.Web.Components
Enter fullscreen mode Exit fullscreen mode

To initialize Fluxor when the application is started, we add this to the very top of the App.razor file:

<Fluxor.Blazor.Web.StoreInitializer />
Enter fullscreen mode Exit fullscreen mode

And finally, update the wwwroot/index.html to include just above the </body> tag:

<script src="_content/Fluxor.Blazor.Web/scripts/index.js"></script>
Enter fullscreen mode Exit fullscreen mode

And with that, Fluxor is ready to be used. Let's update the Counter page to take advantage.

Note: This is the first point at which I diverge from the introductory videos I referred to in part 1 of this series. They both proceed by creating a "Store" folder at the root of the project, with a "Counter" subfolder, and placing the Counter Store files there. I prefer the feature-oriented folder structure that follows.

Creating the Counter Store

Create a folder at the root of the solution named Features, and another folder inside of it named Counter. This will be the place where we keep all of the files related to the Counter.

In the Counter folder create two more folders named Pages and Store. Move the Counter.razor file from the \Pages folder into the \Features\Counter\Pages folder. Your Client project should look like:

Alt Text

Next let's create the Store.

A Fluxor Store consists of 5 pieces:

  1. The State record
  2. The Feature class
  3. One or more Action classes
  4. The Reducers class
  5. The Effects class

Note: This is the second place where I significantly diverge from the introductory videos. They follow the common C# convention of 'one class per file', which I usually also follow, but I think this situation, where none of these classes should really exist without all of the others, is a good example of when it's fine to group multiple classes together in the same file.

In the \Features\Counter\Store folder create a file named CounterStore.cs. Remove the class CounterStore; we're not going to need it. This file will instead hold all of the Store pieces listed above.

In the new CounterStore file, add a record to store the state of the Counter. We only need to track one property: the current count.

public record CounterState 
{
    public int CurrentCount { get; init; }
}
Enter fullscreen mode Exit fullscreen mode

Note the CurrentCount property doesn't have a setter. It's inherently read-only, as is the intention in the Flux pattern.

Below the state we add the Feature class to expose the CounterState to Fluxor. This will require a using Fluxor; statement at the top of the file.

public class CounterFeature : Feature<CounterState>
{
    public override string GetName() => "Counter";

    protected override CounterState GetInitialState()
    {
        return new CounterState 
        {
            CurrentCount = 0
        };
    }
}

Enter fullscreen mode Exit fullscreen mode

The Fluxor class Feature<T> has two abstract methods we must implement. One returns the name that Fluxor will use for the feature, and here I recommend a simple human-readable string that will uniquely identify the feature for this application. The second sets up the initial state of the State. We'll start with a CurrentCount of zero.

Now we need an Action to dispatch when the button is clicked, instructing the store to increment the counter. Below the Feature, add this class:

public class CounterIncrementAction {}
Enter fullscreen mode Exit fullscreen mode

This class doesn't need any properties, since its name includes all the information needed by the Store to know what to do.

Note: I recommend a naming convention for Actions in the form of FeatureActivityAction where Feature in this case is "Counter", Activity is "Increment", and Action distinguishes the class as an Action. The reason for this is that eventually you may have a lot of Features, and if many of them have an Action class named public class Initialize {} then there is a decent chance of name clashes and confusion later. Naming your Actions this way keeps that from happening and makes it easy to keep track of all the pieces of the Store.

And finally, we need a Reducer method to handle the CounterIncrementAction when it is dispatched. Add this class to the Store file:

public static class CounterReducers 
{
    [ReducerMethod(typeof(CounterIncrementAction))]
    public static CounterState OnIncrement(CounterState state) 
    {
        return state with
        {
            CurrentCount = state.CurrentCount + 1
        };
    }
}
Enter fullscreen mode Exit fullscreen mode

Note that this is a static class, and the ReducerMethod is a static method. This is intentional since a Reducer method should be a pure method with no side effects. It takes in the current State (the CounterState state parameter) and returns a new State with new values based on the Action it's handling. The Reducers class itself contains no state.

There are two ways to declare a ReducerMethod as it relates to the Action it is subscribing to. The most common way is to provide both the State and the Action as parameters to the method, like so:

[ReducerMethod]
public static CounterState OnIncrement(CounterState state, CounterIncrementAction action) 
{
    /// code
}
Enter fullscreen mode Exit fullscreen mode

However, in the case of an Action with no payload, such as with CounterIncrementAction, the action parameter will not be referenced in the body of the method. Depending on your compiler settings, this may result in an 'unused parameter' warning. The alternative [ReducerMethod(typeof(Type))] attribute that takes an Action Type is a way of providing the same behavior without the compiler warning.

Note: I recommend a naming convention of OnActivity for the names of the Reducer methods. The name of the Reducer class and methods don't really matter to Fluxor, as they're identified by scanning for [ReducerMethod] attributes, but having a consistent and meaningful name is useful to the developers working on the application. Having a FeatureActivityAction action handled by an OnActivity Reducer method is easy to remember and understand.

With all of these pieces in place, the CounterStore.cs file should look like this:

using Fluxor;

namespace BlazorWithFluxor.Client.Features.Counter.Store
{
    public record CounterState 
    {
        public int CurrentCount { get; init; }
    }

    public class CounterFeature : Feature<CounterState>
    {
        public override string GetName() => "Counter";

        protected override CounterState GetInitialState()
        {
            return new CounterState 
            {
                CurrentCount = 0
            };
        }
    }

    public class CounterIncrementAction {}

    public static class CounterReducers 
    {
        [ReducerMethod(typeof(CounterIncrementAction))]
        public static CounterState OnIncrement(CounterState state) 
        {
            return state with
            {
                CurrentCount = state.CurrentCount + 1
            };
        }
    }

}
Enter fullscreen mode Exit fullscreen mode

Updating the View

With the CounterStore in place, we're ready to update the Counter.razor page to use it.

In the Counter.razor file, just under the @page "/counter" directive, add:

@inherits FluxorComponent
Enter fullscreen mode Exit fullscreen mode

This allows the Counter component to access the Fluxor resources.

Since we're going to need to reference the CounterStore, we need to add a using statement:

@using BlazorWithFluxor.Client.Features.Counter.Store
Enter fullscreen mode Exit fullscreen mode

We'll need to access the CounterState, and in order to dispatch Actions we'll need a Fluxor Dispatcher, so we inject these:

@inject IDispatcher Dispatcher
@inject IState<CounterState> CounterState
Enter fullscreen mode Exit fullscreen mode

We remove the local count property and replace it with a reference to @CounterState.Value.CurrentCount (the IState<T> wrapper exposes the wrapped state object using the .Value property) and replace the counter++ with a call to Dispatcher.Dispatch(new CounterIncrementAction());. The resulting file should look like:

@page "/counter"
@inherits FluxorComponent

@using BlazorWithFluxor.Client.Features.Counter.Store

@inject IDispatcher Dispatcher
@inject IState<CounterState> CounterState

<h1>Counter</h1>

<p>Current count: @CounterState.Value.CurrentCount</p>

<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

@code {
    private void IncrementCount()
    {
        Dispatcher.Dispatch(new CounterIncrementAction());
    }
}
Enter fullscreen mode Exit fullscreen mode

If we build and run the application, we should see the fruits of our labor:

Alt Text

Other components

Once the CounterStore is set up and running, it's not just the Counter.razor component that has access to it; any component in the application can reference it. As an example, let's the CurrentCount to the Nav Menu.

Update the \Shared\NavMenu.razor component to include the necessary Fluxor hooks at the top of the file:

@inherits FluxorComponent
@using BlazorWithFluxor.Client.Features.Counter.Store
@inject IState<CounterState> CounterState
Enter fullscreen mode Exit fullscreen mode

Then change the NavLink label from Counter to Counter (@CounterState.Value.CurrentCount), getting a read-only reference to the CurrentCount property of CounterState.

Build, run, and voilà:

Alt Text

It's that easy.

ReduxDevTools

Ok, one more thing before moving on to the more advanced stuff.

Fluxor also supports the ReduxDevTools browser extension. To enable it, first install the NuGet package Fluxor.Blazor.Web.ReduxDevTools:

Install-Package Fluxor.Blazor.Web.ReduxDevTools
Enter fullscreen mode Exit fullscreen mode

And enable it in the Program.cs by adding .UseReduxDevTools() to the AddFluxor options like so:

builder.Services.AddFluxor(o => o.ScanAssemblies(typeof(Program).Assembly).UseReduxDevTools());
Enter fullscreen mode Exit fullscreen mode

After all of these steps, the solution should look like this branch of the demo repository.

In the next part of this series, I will "featurize" the template's Weather Forecasts component while using a Fluxor Effect method.

Discussion (1)

Collapse
momboman profile image
MomboMan

I much prefer putting all the related classes (WeatherFeature, WeatherReducers, WeatherEffects, ...) in the same file. One place to go to fix, add or change things.

Craig