Blog

Redux in Blazor: Fluxor vs BlazorSignalStore compared

Blazor
Fluxor
State Management
.NET

The same feature built with Fluxor and with BlazorSignalStore: setup, files, re-renders, async calls, tooling and when each one makes sense.

Redux in Blazor: Fluxor vs BlazorSignalStore compared

When people search for Redux in Blazor they end up at Fluxor, the most used Flux/Redux library in .NET. I wrote BlazorSignalStore, which solves the same problem with signals instead of actions and reducers. I’m obviously not neutral, so instead of giving an opinion I’m going to build the same feature with both and show the code.

The feature: a product list loaded from an API, with a loading indicator, a text filter and a counter of visible products.

Fluxor

Setup is in Program.cs plus a component that initializes the store:

builder.Services.AddFluxor(options => options
.ScanAssemblies(typeof(Program).Assembly)
.UseReduxDevTools());
<!-- MainLayout.razor -->
<Fluxor.Blazor.Web.StoreInitializer />

The state is an immutable record marked as a feature:

[FeatureState]
public record ProductsState
{
public bool IsLoading { get; init; }
public IReadOnlyList<Product> Products { get; init; } = [];
public string Filter { get; init; } = "";
private ProductsState() { }
}

Each thing that can happen is an action, and each action has a reducer that returns the new state:

public record LoadProductsAction;
public record LoadProductsSuccessAction(IReadOnlyList<Product> Products);
public record SetFilterAction(string Filter);
public static class ProductsReducers
{
[ReducerMethod(typeof(LoadProductsAction))]
public static ProductsState OnLoad(ProductsState state) =>
state with { IsLoading = true };
[ReducerMethod]
public static ProductsState OnLoadSuccess(ProductsState state, LoadProductsSuccessAction action) =>
state with { IsLoading = false, Products = action.Products };
[ReducerMethod]
public static ProductsState OnSetFilter(ProductsState state, SetFilterAction action) =>
state with { Filter = action.Filter };
}

The API call can’t go in a reducer because reducers must be pure, so it goes in an effect:

public class ProductsEffects(IProductApi api)
{
[EffectMethod(typeof(LoadProductsAction))]
public async Task HandleLoad(IDispatcher dispatcher)
{
var products = await api.GetAllAsync();
dispatcher.Dispatch(new LoadProductsSuccessAction(products));
}
}

And the component inherits from FluxorComponent, which handles the subscription:

@inherits Fluxor.Blazor.Web.Components.FluxorComponent
@inject IState<ProductsState> State
@inject IDispatcher Dispatcher
<input value="@State.Value.Filter"
@oninput="@(e => Dispatcher.Dispatch(new SetFilterAction(e.Value?.ToString() ?? "")))" />
@if (State.Value.IsLoading) { <p>Loading...</p> }
<p>@Visible.Count products</p>
@foreach (var p in Visible) { <ProductCard Product="p" /> }
@code {
private IReadOnlyList<Product> Visible =>
State.Value.Products.Where(p => p.Name.Contains(State.Value.Filter, StringComparison.OrdinalIgnoreCase)).ToList();
protected override void OnInitialized()
{
base.OnInitialized();
Dispatcher.Dispatch(new LoadProductsAction());
}
}

Four types of pieces (state, actions, reducers, effects), usually in four files.

BlazorSignalStore

Setup is one line:

builder.Services.AddSignalStore<ProductsStore>();

The state, the derived values and the operations live in the store:

public class ProductsStore : StoreBase
{
private readonly IProductApi api;
public Signal<bool> IsLoading { get; } = new(false);
public Signal<IReadOnlyList<Product>> Products { get; } = new([]);
public Signal<string> Filter { get; } = new("");
public Computed<IReadOnlyList<Product>> Visible { get; }
public ProductsStore(IProductApi api)
{
this.api = api;
Visible = new Computed<IReadOnlyList<Product>>(() =>
Products.Value.Where(p => p.Name.Contains(Filter.Value, StringComparison.OrdinalIgnoreCase)).ToList(),
Products, Filter);
}
public async Task LoadAsync()
{
IsLoading.Value = true;
Products.Value = await api.GetAllAsync();
IsLoading.Value = false;
}
}

The component subscribes to the signals it reads:

@inject ProductsStore Store
<input value="@filter()" @oninput="@(e => Store.Filter.Value = e.Value?.ToString() ?? "")" />
@if (isLoading()) { <p>Loading...</p> }
<p>@visible().Count products</p>
@foreach (var p in visible()) { <ProductCard Product="p" /> }
@code {
private Func<string>? filter;
private Func<bool>? isLoading;
private Func<IReadOnlyList<Product>>? visible;
protected override async Task OnInitializedAsync()
{
filter = this.useSignal(Store.Filter);
isLoading = this.useSignal(Store.IsLoading);
visible = this.useSignal(Store.Visible);
await Store.LoadAsync();
}
}

One class for the store and nothing else. The async call is a normal method.

What changes in practice

Amount of code. Fluxor needs a type for each event and a function for each transition. In exchange, the state can only change in reducers and everything that happens has a name. In BlazorSignalStore any code with access to the store can write a signal, which is shorter and also easier to misuse in a large team.

Re-renders. A FluxorComponent re-renders when the feature state changes, whatever part of it changed. Fluxor has IStateSelection<TState, TValue> to subscribe only to a part of the state and avoid that, but you have to use it explicitly. In BlazorSignalStore the subscription is per signal by default, and Computed values are only recalculated when one of their dependencies changes. In the demo of the library, with a nine-field form, that meant about 50% fewer re-renders than the Microsoft state container. I haven’t measured Fluxor with selectors, so I won’t give a number there.

Async work and side effects. Fluxor separates them into effects, which keeps reducers testable and centralizes the calls to the outside world. In BlazorSignalStore they are methods in the store, which is more direct but leaves the discipline to you.

Tooling. Here Fluxor wins clearly. The Redux DevTools integration lets you see every action, the state before and after, and travel back in time. It has middleware for logging and years of use in production. BlazorSignalStore is a young library, without DevTools, and with a much smaller user base.

Learning curve. Anyone who has used Redux, NgRx or Vuex recognizes Fluxor immediately. Signals are closer to what Angular, Solid or Vue offer today, and someone who has never used either usually finds signals faster to pick up.

When I would use each one

I’d choose Fluxor for a large app with several people touching the same state, where tracing every change and having DevTools is worth more than writing less code, or for a team that already works with Redux in other front ends.

I’d choose BlazorSignalStore for forms and screens with a lot of interdependent state, where re-renders matter, for small and medium apps where Fluxor’s ceremony doesn’t pay off, and when I want derived values to stay up to date without thinking about it.

And for many apps, neither: a state container or the URL is enough. I go through all the options in Blazor state management: every option and when to use it.

The library is on NuGet (dotnet add package BlazorSignalStore) and the code and the demo are on GitHub.


Comments

No comments yet. Be the first!

Leave a Comment

Your name and comment will be published once approved. More in the privacy policy.