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:
- A built-in system for the pub-sub through
INotifyPropertyChanged
- Pattern matching
- A Type system
- 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:
- Store is injected into a client
- Client Dispatches a request for state change
- Store updates state
- Store notifies subscribers of state change
- 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!