24 June 2023

(This post was inspired by Steven Wittens’ very compelling Climbing Mount Effect series, and the subsequent months-long reactivity rabbit hole it sent me down.)

It’s well-known that state management in programs can lead to a lot of problems. Spaghetti machines of far too many transitions, orchestrated by widely-visible variables, lead to hard-to-debug, hard-to-change code.

When domain logic gets unwieldy, we are used to wrapping things up in classes, functions, single responsibility principles, to manage the mess. The minutiae of state rarely receives the same treatment. How can we even wrap up something so universal as setting an object attribute or changing a global variable?

Let’s take a look at how state management ends up growing out of control.

When we have long-lived entities in our programs, we often find ourselves writing lifecycle code: callbacks which handle certain state transitions, or provide behaviour in certain external conditions. A ubiquitous example is the constructor and destructor, but the pattern is everywhere, for example in live entities in a video game:

class Player:
    def load(self):
        """Initialise the player in the level."""
        ...
    def update(self, dt):
        """Update state per frame."""
        ...

It’s also not uncommon for an object to have children—objects it owns in instance variables—which share its lifecycle. In a game, we might have a collection of entities in a scene. The scene also loads and updates, but mostly just has to thread calls down to its children:

class MyScene:
    def load(self):
        self.player = Player()
        self.player.load()
    def update(self):
        self.player.update()

We also end up having to pass down state changes.

class EvilMinion:
    def load(self):
        self.sprite = SpriteRenderer()
        self.sprite.set(calm_minion_sprite)
        self.aggro = False
    def set_aggro(self, aggro):
        if aggro:
            self.sprite.set(angry_minion_sprite)
        else:
            self.sprite.set(calm_minion_sprite)
        self.aggro = aggro

We’re starting to see some entanglement. EvilMinion setters just forward onto child SpriteRenderer setters. We have our own instance variables which correspond exactly to instance variables of our held objects. In both cases, a fragile stack of duplicated state grows and must be coordinated. Maybe we also have to manage history too, so ad hoc _last_x variables begin to crop up all over the place.

In both cases, the issue is that the child’s lifecycle is not encapsulated from the parent object. Dealing with lifecycle becomes a global concern, all the way up the stack of classes: we have to dot i’s and cross t’s at every level. Messing this up is easy and can causes serious state mismatch errors (think memory leaks and use-after-frees).

What we want is a way to specify child objects once and have them wired up automatically.

C++ has a feature where if you don’t define one of the special lifetime methods on a class, it defaults to calling those methods on each of the instance variables.

// When a foo object is copied, moved, or destroyed,
// the owned vector v will be too.
class foo {
    int x = 10;
    std::vector<int> v = {1, 2, 3, 4, 5};
};

This lets us fully delegate lifecycle concerns to our child objects. We don’t have to write any e.g. memory management methods for most classes; instead we just use self-contained building block components, like std::vector or std::unique_ptr.

Doing this ourselves might look like:

class DelegatingEntity:
    def load(self):
        for c in self.children:
            c.load()
    def update(self):
        for c in self.children:
            c.update()

class MyScene(DelegatingEntity):
    def __init__(self):
        self.children = [Player()]

But what if we wanted to change the children as the object changes? We could have methods to manually remove and add children, but then we’d have to deal with state transitions again. What if we could just re-run a method when we need to update the object? That way we specify a scene fully declaratively via a pure function.

class MyScene(DelegatingEntity):
    def get_children(self):
        return [Player()]

To make this work, we’ll need the built class to manage re-running the method and comparing the results between calls. Any objects of the same type which can be matched up can stay; provided that any input arguments of the new object are copied across to the old instance’s attributes. New additions or removals need to be loaded and deleted respectively.

Once we have that— wait a second… a declarative method which describes child objects and reconciles between calls?

class Greeting extends Component {
  render() {
    return <h1>Hello, {this.props.name}!</h1>;
  }
}

It’s React. We basically just made React!

We skipped lots of detail, but the core idea is there. React is only a tree of HTML elements by coincidence; in general, it’s a system for declaratively specifying state.

To unify the idea, instead of game entities or HTML elements, the building blocks are “reversible functions” or “effects”; objects with do/undo (mount/unmount) methods. These are fully generic ways of describing an undoable side-effect, such as registering an event handler or adding a node to a global tree. So, you can think of a <div> element in React JSX as an object which can mount/unmount an HTML <div> at the current tree node. We make bigger reversible objects (components) out of them with our delegation pattern. All the doing and undoing is handled by a single, top-level system, and not at all by the programmer.

The issues we had with our naive approach are gone. Describing child state is now fully declarative: there is no manual management of lifecycles or passing through changes. We just pass down normal values describing our state through the tree, and the delegation logic takes care of the synchronising.

class MyScene(DelegatingEntity):
    def get_children(self):
        return [
            Player(),
            EvilMinion(),
            EvilMinion(),
        ]

class EvilMinion(DelegatingEntity):
    def get_children(self):
        ...
        sprite = angry_minion_sprite if self.props.aggro else calm_minion_sprite
        return [
            SpriteRenderer(sprite)
        ]

The idea starts to feel a lot more applicable than to just Web UIs. The best thing is that this isn’t a dark art, and just a few hundred lines of code can get you the skeleton of a reactivity system. You can take this pattern to other interactive, stateful domains. Pretty cool, right?

(P.S. Nowadays, React components are written as functions with “hooks” like useState. This is just a way of having class-like object state inside a function. The React runtime has a global store for all the hook data and it swaps out which are visible depending on the currently running component in the tree. The advantage is ergonomics; the closest equivalents to hooks in class land are mixins, but these are much uglier to use for this.)