Home

A Lean Redux

I have started using .NET MAUI at work this past week. MAUI is the successor to Xamarin, but draws on much of the same tooling and paradigms. In its present state the documentation is pretty sparse, and it seems many enterprise-level issues are left for developers to resolve. That’s no issue with me, though the docs can be surprisingly unhelpful, leaving one frustrated.

One omission I noticed early on is the lack of a state management pattern. MAUI provides no tooling itself and does not recommend how to manage state between UI components.

State Management

When I’ve written React apps I have found it valuable to make use of the Redux pattern for any state management above a certain complexity. That is, I typically won’t use it to manage state in an isolated form, but as soon as that form begins to interact with other state, and particularly across components, it becomes a suitable pattern.

I do, however, think Redux has issues, particularly in the amount of boilerplate required, and the use of strings as the reference to mutations on state. Passing anything other than the action itself to mutate state leads to seemingly unnecessary code:

function reducer(state, action) {
    switch (action.type) {
    case 'increment':
        return {count: state.count + 1};
    case 'decrement':
        return {count: state.count - 1};
    default:
        throw new Error();
    }
}

The problem with the above, taken from the React docs, is that in a sufficiently complex case, this switch statement would become unreadable. This solution does not scale, yet Redux is supposed to be a scaling solution.

Now some of this may be limitations of Javascript, which Redux is written for, but even in a strongly-typed language like C#, not passing the action itself increases boilerplate:

public static int Execute(int previousState, IAction action)
{
    if (action is IncrementAction)
    {
	return previousState + 1;
    }

    if (action is DecrementAction)
    {
	return previousState - 1;
    }

    return previousState;
}

Or, more succinctly:

public static int Execute(int previousState, IAction action)
{
    return action switch
    {
	IncrementAction => previousState + 1,
	DecrementAction => previousState - 1,
	_ => previousState
    };
}

Every time a new action is required, one has to modify Execute and create a new implementation of IAction. IAction implementations are completely empty, which I think violates the principle of least astonishment: an implementation of an IAction would be expected to define the behaviour of the interface, not some isolated method called Execute. We also effectively cannot seal Execute, marking it as closed for modification, because new state mutations will always be required.

The above example is taken from the no longer maintained Redux.NET project, and are not meant as criticisms of that project, but as a criticism of how Redux centralises state mutations, but then has you reference them indirectly. I don’t think the above issues can be resolved with a different language.

Be more abrupt

I guess I’m asking Redux to be more direct, to not have to skirt around the topic of mutating state. After all, as the caller, I’m the one making the command to do what I say, in which case It’d be better to pass what I want to have happen directly.

Perhaps I am just asking for React’s useState, with lambdas to setState defined in a central place.

const actions = {
    increment: _ => _ + 1,
    decrement: _ => _ - 1
};

const [state, setState] = useState(0);

setState(actions.increment);

This would work within a component, but it doesn’t solve the issue of state managed outside of the components. We’d need to wrap setState into a new hook to achieve that, but it is perhaps worth playing around with.

A solution with strong typing

For MAUI I could make use of C#’s Func and Action types to allow callers to pass delegates to the state manager.

sealed class State<T>
{
    private T _state;

    public void Dispatch(Mutator<T> action)
    {
	_state = action(_state);
    }
}

Dispatch mutates the internal state by reassigning it. We can then implement IObservable to allow observers to react to changes.

// inside State<T>
private IImmutableList<IObserver<T>> _observers = ImmutableList.Create<IObserver<T>>();

public void Dispatch(Mutator<T> action)
{
    try
    {
	_state = action(_state);
	_observers.ForEach(_ => _.OnNext(_state));
    }
    catch (Exception ex)
    {
	_observers.ForEach(_ => _.OnError(ex));
	throw;
    }
}

As the underlying T value is not accessible outside State, Dispatch is the only way to mutate and ensure changes propagate to subscribers. This is not completely encapsulated: we can’t avoid that T may be a reference type, but we can constrain how callers mutate state in a way to alert subscribers. If a caller mutates the reference type instance directly, no subscribers will know about such a change. The change could therefore be considered invalid.

Accessing state

As the underlying value is only accessible from Dispatch, the question becomes how to access the state to make use of it? For this, State provides a selector that will return a new instance of State.

sealed class State<T>
{
    public State<R> Select<R>(Func<T, R> selector) => new(selector(_state));
}

But wait, that doesn’t help because the return value still encapsulates the underlying state. This is by design, because now the only way to access the underlying state is within the selector you provide to Select. This forces the caller to group their state access into calls to Select. Combined with Dispatch, the intention is for it to be unambiguous when:

  1. You are mutating state
  2. You are accessing state

Locking it down

Unfortunately, because Select exposes the underlying value, which may be a reference type, an unknowing developer could mutate state within Select, introducing bugs as modifications do not propagate to subscribers.

There are no language features available to prevent this. Taking deep copies of reference types in C# is non-trivial and potentially very slow: you wouldn’t want Select to crawl because your state is large and heavily nested. Instead I did experiment with writing a Roslyn analyzer to detect when modifications are being made to the underlying value within the selector. As yet I haven’t been able to get it to work satisfactorily, but the idea was to display an analyzer error if assignments are detected. This would be noisy enough for most developers to notice.

For now, I figured it was “enough” to write a doc string explaining the correct usage. This is brittle, but at least the restriction is no longer implicit.

/// <summary>
/// Exposes the underlying state <typeparamref name="T"/> for access.
/// </summary>
/// <remarks>
/// This method should <b>not</b> be used to modify the state of <typeparamref name="T"/>.
/// Any modifications made here will not propagate to subscribers.
/// To modify state, use <see cref="Dispatch(Mutator{T})"/>.
/// </remarks>
/// <typeparam name="R"></typeparam>
/// <param name="selector"></param>
/// <returns>A new state instance.</returns>
public State<R> Select<R>(Func<T, R> selector) => new(selector(_state));