Demystifying State Management

State management is the probably the hardest aspect of frontend and backend programming, let's demystify it using some techniques from Engineering Control Theory. 29 June 2019

# Background

Over the years, I've had the privilege of working with many production react  + redux  codebases across a number of industries. Most start off simple, but code complexity seemed to grow much faster than feature complexity. Much of this can be attributed to misunderstanding of how state management can/should be done. I hope this article demystifies state management for you.

# State transition equations

In Control Theory engineering, one pair of equations is often used to describe dynamical systems :

x[t+1] = Ax[t] + Bu[t]
y[t ] = Cx[t]

Where it defines: u[t] the input at time t, x[t] the state at time t, y[t] the output at time t, and A, B, and C the relevant transformation functions. Control Theory goes on to explore concepts of controllability  and observability , and more, but we won't go that far in this article. Suffice to say, that there is a whole field of study behind this pair of equations.

Let's see what we can learn from it.

# A classic: the todo app

Consider the classic programming challenge — the todo-app. Its inputs u[t] may be a form input, an XHR response, or a push notification. These inputs may not be in a consistent or desired format, and needs to be normalised before it can be stored as local state x[t], say an array of todo items. Therefore, the next state x[t+1] is a transformation A of the current state x[t] combined with a transformation B of the input u[t]. Finally, the output y[t] may be one of: pending todos, archived todos, or the next todo. These can be extracted from the state x[t] with transformation C.

# How would this look like in redux?

redux  is the incumbent leader in state management for frontend applications. Its documentation emphasizes actions and reducers and how it works under the hood. Let's focus on the transformations instead.

The transformation B is most commonly performed with an action dispatch. Ie, the return value (except for type: `ADD_TODO`) of the following function is effectively Bu[t], the transformed input.

function addTodo (payload) {
return {
type: "ADD_TODO",
todo: parse(payload)
}
}

The transformation A is then performed with a reducer, which combines the previous state and already transformed input to return the next state Ax[t] + Bu[t].

function reducer (state = [], action) {
switch (action.type) {
case "ADD_TODO":
return [ ...state, action.todo ]
default:
return state
}
}

Finally, the transformation C is performed with one of many selectors, which returns the transformed state Cx[t]. When used with react-redux , this is what goes into connect() higher order component, or the useSelector() hook.

function pendingTodo (state) {
return state.filter(todo => !!todo.isPending)
}

function nextTodo (state) {
return state[0]
}

# Common modelling errors

One very common modelling error is to omit the use of selectors, and the output y[t] is exactly the value of the state x[t]. Mathematically, C = I, the identity function.

x[t+1] = Ax[t] + Bu[t]
y[t ] = x[t]

Not only does this unnecessarily grow the state, it grows the size of the transformation A. Pending todos, archived todos, or the next todo are duplicated within the state, and can easily get out of sync.

Conversely, another common error is to store the input u[t] (usually XHR responses) directly into the state x[t]. Mathematically, B = I, the identity function.

x[t+1] = Ax[t] + u[t]
y[t ] = Cx[t]

Doing so increase the complexity of the output transformation C, and overly couples the input and output formats.

# Much more than shifting transformations

Moving where the transformation happens increases semantic-ness. The bulk of state manipulation should live in A; while B and C are used only to transform the input and output respectively.

The state x[t] directly represents the domain model , and is not confused with input or output representations.

# What about mobx?

mobx  is another popular Javascript state management library. It heavily emphasizes reactivity by introducing concepts such as observable variables (proxies under the hood), actions to modify them, and computed attributes that are reactively updated.

Most mobx practitioners are probably already implementing the state equations without realising it. In the example below, the @action.bound annotated function applies the first line of the state transition equations. Then, the @computed annotated functions apply the second line of the state transition equations.

import { action, computed, observable } from "mobx"

class Todo {
@observable todos = []

@action.bound addTodo (payload) {
this.todos.push(parse(payload))
}

@computed get pendingTodo () {
return this.todos.filter(todo => !!todo.isPending)
}

@computed get nextTodo () {
return this.todos[0]
}
}

The relationships are obvious, and very succinctly represented.

# Not just frontend

Frontend applications is not the only place where these equations could be applied. Here's an example in Ruby on Rails. See if you can spot where the transformations A, B, and C are.

class TodosController < ApplicationController
def add # mounted to a POST route
@todo = Todo.new(params.require(:todo))
@todo.save
redirect_to @todo
end

def pending # mounted to a GET route
@todos = Todo.pending
end

def next # mounted to a GET route
@todo = Todo.pending.first
end
end

class Todo < ApplicationRecord
scope :pending, -> { where(pending: true) }
end

# Removing the state

It is illustrative to consider what happens when the state x[t] is removed. Without state, the input u[t] passes through 3 distinct transformations to become the output y[t].

y[t] = CABu[t]

As an aside, Control Theory calls these systems time-invariant . But again, we're not going that way in this article.

The first transformation B changes the input format into a local format more suitable for processing. Then the second transformation A processes the data. Finally, the third transformation C changes it to the desired output format. Experienced database administrators may recognise this as the Extract-Transform-Load  pattern.

# Summary & Conclusion

All the above should be intuitively obvious. It is nonetheless comforting to know that behind this software pattern, is a whole field of engineering study.

Users of redux, unfortunately often miss the opportunity to apply this pattern because of the strong emphasis on actions and reducers, without a corresponding emphasis on selectors. mobx users, however more instinctly see the relationships from input, to state, to output transformations. Even Ruby on Rails, or other backend MVC framework developers are accustomed to the pattern, because MVC encourages this flow.

Finally, how the state x[t] is stored greatly affects how complex the application will become. While it is tempting to use the same format as the input u[t], or the output y[t]; it is usually far more meaningful to store it in the corresponding domain model.