blazor-state-patterns-flux-redux-reactive

State Patterns in Blazor: Reactive Services and Flux

  • 4 min

The unidirectional data flow is a pattern where state changes through controlled actions and is then projected back onto the view.

Our simple State Container already encapsulated a counter, but a state with collections and many operations can end up exposing mutations like these:

// The scalability problem
State.Carrito.Add(producto); // Component A adds
State.Carrito.Clear();       // Component B clears without warning
State.Carrito = null;        // Component C breaks the app
Copied!

In a small application, this is acceptable. But in an enterprise application, this becomes a debugging nightmare. If the state changes unpredictably, bugs are impossible to trace.

Flux and Redux popularized a stricter way to organize these changes through a single predictable flow.

The Flux principle: a single path

The idea is simple: Components should NEVER modify the state directly.

Instead, components can only “request” that something be done (an Action). That action is processed by a central handler (Store), which decides how to update the state. Finally, the new state flows to the components.

  1. View: The user clicks “Buy”.
  2. Action: A message is dispatched: ActionBuyProduct(id: 5).
  3. Store: Receives the message, checks stock, adds to the list, and creates a new state.
  4. View: Updates upon receiving the new immutable state.

Reactive Services in Blazor

We don’t need to install heavy JavaScript libraries for this. In C#, we can implement a clean version of this pattern using Reactive Services.

The key is encapsulating mutations.

Read-only state

The state should not be modifiable from outside. We use read-only properties (get) or immutable lists (IReadOnlyList).

public class CarritoStore
{
    // Private internal state
    private List<Producto> _items = new();

    // READ-ONLY public state
    // Nobody can do State.Items.Add(...) from outside
    public IReadOnlyList<Producto> Items => _items.AsReadOnly();

    public decimal Total => _items.Sum(x => x.Precio);

    // Notification event
    public event Action? OnChange;
}
Copied!

Actions expressed as methods

Instead of letting the component touch the list, we expose methods that represent user intentions, not raw data operations.

    // Method acting as "Dispatcher" / "Reducer"
    public void AgregarProducto(Producto producto)
    {
        if (producto == null) return;

        // Business logic goes here (validations, limits, etc.)
        if (_items.Count >= 10)
        {
            Console.WriteLine("Cart is full");
            return;
        }

        _items.Add(producto);

        // Notify the change
        NotifyStateChanged();
    }

    public void VaciarCarrito()
    {
        _items.Clear();
        NotifyStateChanged();
    }

    private void NotifyStateChanged() => OnChange?.Invoke();
Copied!

With this small change, we have gained a lot:

  • Control: Only the Store class knows how to modify the data.
  • Debugging: If you set a breakpoint in AgregarProducto, you’ll know exactly when and who adds things.
  • Validation: Corrupt data cannot enter the state.

Libraries: Fluxor

If your application is truly complex (hundreds of screens, complicated workflows), writing these services by hand can become tedious.

For those cases, Fluxor is a popular and maintained option for applying Flux/Redux in .NET and Blazor applications.

Fluxor forces you to separate the code into very small pieces:

  1. Feature: Defines the initial state.
  2. Action: Simple classes (records) that define messages (public record AddItemAction(Item item);).
  3. Reducer: Pure functions that take Old State + Action and return the New State.
  4. Effect: Logic for side effects (API calls).
// Conceptual Fluxor example
// The component only does this:
Dispatcher.Dispatch(new AddItemAction(nuevoProducto));
Copied!

Fluxor adds structure and more pieces, but it can integrate with Redux DevTools to inspect actions and states during debugging.

Which pattern should I use?

My practical recommendation would be this:

  1. Simple State Container: For small apps, demos, or non-critical data (e.g., is the sidebar open or closed?).
  2. Reactive Services: When you want to encapsulate rules and notifications without adopting a full framework.
  3. Fluxor: When the action history, reducers, effects, and rigid conventions justify the additional code.