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 appIn 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.
- View: The user clicks “Buy”.
- Action: A message is dispatched:
ActionBuyProduct(id: 5). - Store: Receives the message, checks stock, adds to the list, and creates a new state.
- 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;
}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();With this small change, we have gained a lot:
- Control: Only the
Storeclass 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:
- Feature: Defines the initial state.
- Action: Simple classes (records) that define messages (
public record AddItemAction(Item item);). - Reducer: Pure functions that take Old State + Action and return the New State.
- Effect: Logic for side effects (API calls).
// Conceptual Fluxor example
// The component only does this:
Dispatcher.Dispatch(new AddItemAction(nuevoProducto));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:
- Simple State Container: For small apps, demos, or non-critical data (e.g., is the sidebar open or closed?).
- Reactive Services: When you want to encapsulate rules and notifications without adopting a full framework.
- Fluxor: When the action history, reducers, effects, and rigid conventions justify the additional code.