Encapsulating Application Logic with Reactors

Michael Tiller
6 min readJan 13, 2016

--

Having written up some material recently about application architectures in general and also routing in particular, I wanted to write something focused exclusively on what I call “Reactors”.

By the name, you might assume that “Reactors” had something to do with React. But, in fact, the opposite is true in some sense. Reactors were something I’ve used to try to keep “the view” (e.g., React) out of my application logic as much as possible. The name “Reactors” came about when trying to combine Redux with the “actors” mentioned in this
article
(not to be confused with these actors).

The Problem

The main issue I was dealing with when I started using this pattern was in handling transitions between different routes. The point is that if routes are represented in your redux store, then you’d like to have a way to inject application logic when you switch routes. The example I always invoke here is for loading data that is needed by a particular route.

An essential thing to recognize about Reactors is that they rely on knowledge of the previous state as well as the current state. If I only paid attention to the current state, I wouldn’t be able to determine if I’ve just entered a given route and need to load the data necessary for that route or whether I was already in that route and have already made the request to load that data (and I’m simply waiting for some asynchronous response). So the notion of previous state is essential in this context.

Influences

As soon as I started using Redux, I started getting excited about the possibilities associated with it. Having a centralized application state, immutable data and getting rid of side-effects were all very positive directions as far as I was concerned. This essentially led to a mental model of my application as a state machine which was very appealing because it was easier to reason about.

After I started using Redux, I came across this article which inspired me to start thinking about how I could move more of my application logic into the Redux part of my application and away from the React part. This is all part of my grand scheme to streamline application testing by moving application logic out of the view.

Once you agree that application logic doesn’t belong in the view, the natural question then becomes…where do I put it? Of course, I can put it anywhere. But I want it to be as modular and declarative as possible. So I played around to see what kind of patterns emerged. The notion of a Reactor was one such pattern.

State vs. Computed Properties vs. Reactive

Before going further, I want to point out that there is some subtlety here. As I mentioned previously, a Reactor performs some calculations based on previous and current state. It may be the case that the Reactor simply observes these changes and triggers some asynchronous request. But it may also be the case that it synchronously mutates state in response to that change.

This sounds an awful lot like a computed property that could be calculated using ‘reselect’. What distinguishes a Reactor is that it depends both on previous state and current state. In other words, whatever calculation it performs is based on the delta between two states. It also happens that a Reactor is capable of mutating state inline.

This is a subtle distinction and I’m not 100% convinced that we are working with the right set of abstractions here. It turns out that many of the ideas of Redux and React have long existed in control system theory. In React we clearly have properties (‘u’, in control system theory) and states (‘x’, in control system theory). These are treated as first class entities in React. This is discussed implicitly in the Thinking in React introduction to React. In Redux we have a similar mapping with actions, ‘u’, and states, ‘x’. But we don’t really have a first-class notion of outputs (‘y’ in control systems).

These outputs are computed properties. They are the things that we know we can always compute if we know the properties and states. But the issue I see in the Redux world is that we don’t have a way of synchronously tracking these outputs. We can use observers, subscribe listeners or use other reactive programming techniques. But those calculations are reactive, not synchronous.

One thing that Reactors do give us (over these other approaches) is synchronicity. This is because Reactors are implemented in the reducer function. If they can synchronously respond to whatever change they are tracking, they can inject that change inside the reducer. The effect is that you won’t end up with a “double update” of the view (one when the initial state change occurs and one when one or more reactions to the state change occur).

Implementation

This (potentially) synchronous behavior is achieved by wrapping an
existing reducer function with code that checks for specific changes. This kind of “middleware” approach is common for the store, but less so for the reducer. But I think applying it to the reducer makes more sense in some cases.

Conceptually, imagine we have an existing reducer function called ‘f’.
We might come up with another reducer function, ‘f2’, as follows:

function f2(state, action) {
let initialState = state;
let finalState = f(state, action);
return reactor(initialState, finalState);
}

You can see here that a Reactor simply takes a previous state, a current state and then returns a (potentially new) state. But the function `f2` is still a reducer. All of this is internal and there are no side effect (i.e., if you call the function with the same arguments, you’ll get the same result).

This addresses two of the concerns I had with the “actor” model I mentioned earlier. The first is to avoid using ‘subscribe’ because there are really no guarantees about when the subscribers are notified or in what order. This avoids potential issues with dispatching actions back on the store and getting into infinite loops. Neither of those are in play here. The reactor gets called inside the reducer before the store even sees the result. Furthermore, the Reactor only gets one chance to trigger (and if that constraint doesn’t work for you…I think you should rethink your application logic).

It turns out that there are cases where we want to have multiple reactors wrapping the same reducer. This is handled by recursively wrapping them. The actual command (using ‘vada’) would be:

const reducer = wrapReducer(baseReducer, [reactor1, reactor2]);

In practice, this would be equivalent to:

function reducer(state, action) {
let s0 = state;
let s1 = baseReducer(s0, action);
let s2 = reactor1(s0, s1);
let s3 = reactor2(s0, s2);
return s3;
}

Note that ‘reactor2’ sees the difference between the initial state and
what is returned by ‘reactor1’. In other words, whatever changes ‘reactor1’ made to the state are incorporated into the changes seen by ‘reactor2’. This makes the whole process modular (multiple reactors can be composed), synchronous (just like a reducer), functional (no side-effects) and deterministic.

When to use Reactors

As I said before, there is a subtle difference between a computed property and a Reactor. The selectors in ‘reselect’ take the current state and return some value. To avoid confusing Reactors with selectors, I deliberately made the previous state the first argument to the reactor. As such, any reactor is forced to declare the previous state in its argument list. If you don’t use it…you don’t really need a Reactor. If I had put the final state first, I imagined lots of people just using these as a way to compute something based on the current state. For this, they are better served using something like `reselect`.

It is worth noting that the ‘vada’ library includes some features to help you avoid using Reactors as well. A great place to inject computed properties is propagating properties through the component tree in a React application. The ‘react-redux’ library provides the ‘connect’ function. I created a similar function called ‘bindClass’. The main difference is that I wanted something that provided type constraints to avoid forgetting a prop along the way. I wasn’t quite able to come up with type definitions for ‘connect’ that satisfied me. But conceptually, they are very similar. For similar reasons, I also included some memo-ization functions that, again, provide type constraints to avoid losing any type information. Since both these capabilities are relatively simple, I didn’t feel (too) guilty re-implementing them.

Conclusion

Reactors are a useful pattern to encapsulate application logic. The
distinction between a Reactor and an ordinary computed property is
that a Reactor depends on both the current state and the previous
state and it provides a (potentially) new state value for the complete
state.

The fact that Reactors are inlined inside of reducers means that their behavior is deterministic and that they are applied synchronously with state changes. This avoids many of the issues that occur when subscribing to stores.

Although I’m publishing a library and associated documentation on these patterns, I don’t really feel the dust has settled yet. As I mentioned previously, I feel like the handling of state changes and computed properties is currently given a kind of second-class treatment in the Redux approach. So all of this is an attempt to move toward first class abstractions for these things.

--

--