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:
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:
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.
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
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));
and the accompanying using
statement at the top:
using Fluxor;
For convenience, add a couple of Fluxor using statements to the _Imports.razor
file:
@using Fluxor
@using Fluxor.Blazor.Web.Components
To initialize Fluxor when the application is started, we add this to the very top of the App.razor
file:
<Fluxor.Blazor.Web.StoreInitializer />
And finally, update the wwwroot/index.html
to include just above the </body>
tag:
<script src="_content/Fluxor.Blazor.Web/scripts/index.js"></script>
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:
Next let's create the Store.
A Fluxor Store consists of 5 pieces:
- The
State
record - The
Feature
class - One or more
Action
classes - The
Reducers
class - 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; }
}
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
};
}
}
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 {}
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 ofFeatureActivityAction
whereFeature
in this case is "Counter",Activity
is "Increment", andAction
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 namedpublic 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
};
}
}
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
}
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 aFeatureActivityAction
action handled by anOnActivity
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
};
}
}
}
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
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
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
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());
}
}
If we build and run the application, we should see the fruits of our labor:
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
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à:
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
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());
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.
Top comments (2)
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
To me, the use of Fluxor shown here, or even in the examples by its own creator, seems more like a way to build an API of the application. Which may be good or not, but it is no simply a way to manage the state of the applicazione.