Flux in .NET (Part II)

Building a Flux Store with POCO

Rolling your own Flux implementation

In this article, we'll explore how to implement the main mechanism for state management in Flux - the Store. The great thing about Flux is its simplicity and it can be created entirely with plain old C# objects. However, there are several language features that make implementing a Store in C# dead easy compared to JavaScript:

  1. A built-in system for the pub-sub through INotifyPropertyChanged
  2. Pattern matching
  3. A Type system
  4. Immutable data types through record Types

Recap

Flux is a pattern for state management through unidirectional data flow. The Store is the main mechanism that manages the state and processing of state change requests.

From Part I, to implement a store, we need to implement the following interface. However, we can do better by replacing the Subscribe method with an implementation of INotifyPropertyChanged.

public interface IStore<out T> : INotifyPropertyChanged
{
  T State { get; }
  void Dispatch(IFluxAction action);
  // Action Subscribe(Action<T> onNewState);
}

public interface IFluxAction { }

Flux Sequence

You can think of an implementation of the IStore as just an actor. An instance of IStore can be injected into a client (i.e. services or UI controls). These clients can then send messages (AKA IFluxAction) to the Store through the Dispatch method. The clients then receive updates to state by subscribing to the PropertyChangedEvent of the Store.

To reiterate as a sequence of events:

  1. Store is injected into a client
  2. Client Dispatches a request for state change
  3. Store updates state
  4. Store notifies subscribers of state change
  5. Clients react to new state change

Updating State with Reducers

So the next question is how is state changed? The great thing about this architecture is that it's completely up to you. The most common way from the React-Redux ecosystem is through a reducer. To understand what a reducer is, consider the following Linq statement:

var listOfNumbers = ...
var total = listOfNumbers.Aggregate(sum);

A reducer is just an aggregate method. In the above example, sum is the reducer. In the case of Flux, a reducer is an aggregate method that combines the current state and current action to produce the new state. It can be defined with the following delegate:

delegate T Reducer(T state, IFluxAction action);

The JavaScript specification for an Action is a JSON object with two fields called type and payload. The JavaScript Reducer uses a switch-case statement against the type field and applies a function to calculate the new State.

Well, this is .NET! We have Types and Pattern matching. Using the common example of a counter, we can implement this reducer as follows:

// Actions for the Count Domain
public record IncrementCounter() : IFluxAction;
public record DecrementCounter() : IFluxAction;

// Data store for Count
public record Count(int count);

...
// Reducer for Count somewhere else in the code
  public static Count Reduce(Count state, FluxAction action) =>
    action switch {
      IncrementCounter _ => state with { count = state.count + 1 },
      DecrementCounter _ => state with { count = state.count - 1},
      _ => state
    };
...

Since the Reducer is a static function, it can be placed in a static class or as a private static method in the Store itself. The choice is yours. In the React-Redux ecosystem, it is common practice to keep the reducers, actions, and store separate.

There are implementations where the reducers are baked into the Actions. These implementations make the assumption that the relationship between action and reducers is one-to-one. Once again, the choice is yours!

So what? Now we have a function that can calculate new state. That's great, but how does this address the problem of state management?

Updating State

Let's implement this Store using procedural programming! To understand how simple this is, let's just implement the Dispatch method:

...
public T State { get; private set; }
...
public void Dispatch(IFluxAction action)
{
  var newState = Reduce(State, action); // <-- Most important line of code!!!
  if (newState == State) return;
  State = newState;
  OnPropertyChanged(nameof(State));
}
...

Done! If inheritance is your thing, then this could be implemented as an abstract base class. If you want to avoid inheritance like the plague, then this could be implemented as a generic class, where the Reducer is passed in as a collaborator. I've been burned too many times by inheritance, so let's look at the full Store implementation using collaboration:

public sealed class Store<T> : IStore<T>, INotifyPropertyChanged {
  private readonly Reducer<T> _reduce;

  public Store(in T initialState, in Reducer<T> reducer)
  {
    State = initialState;
    _reduce = reducer;
  }

  public T State { get; private set; }

  public void Dispatch(IFluxAction action)
  {
    var newState = _reduce(State, action);
    if (newState == State) return;
    State = newState;
    OnPropertyChanged(nameof(State));
  }

  public event PropertyChangedEventHandler PropertyChanged;

  private void OnPropertyChanged([CallerMemberName] string propertyName = null)
  {
    PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
  }
}

And that is that! The intent of this was to show the basics of a store implementation. We can do better and we will. In the next post, we'll investigate how to further pair this down by using .NET Data Flow.

For those of you that are familiar with Flux, you may be wondering where is the middleware or enhancers. I'll be addressing that topic in a future post. If you have any questions, please feel free to comment. I would appreciate any feedback to improve the clarity of this article. Thanks!

No Comments Yet