State Architecture Patterns in React, Part 3: Articulation Points, react-zine, An Overall Strategy and Flux-duality

Skyler Nelson
11 min readMar 17, 2017

--

This is the third in a series of articles about state architecture patterns in React.

In the previous installment, we discussed the top-heavy architectural pattern and encountered some potential performance traps associated with it.

This installment is about the use of what I call articulation points to enhance top-heavy architectures and prevent React from doing unnecessary work. I’ll introduce the topic, then show how to use the library zine to construct a top-heavy architecture and enhance it with articulation points. Finally, I’ll outline a simple overall architectural strategy based on what we’ve learned so far and conclude with a final note about the relationship between this architectural strategy and the Flux pattern.

Articulation Points, or: Pub/Sub To The Rescue

I started off this series with the following claim:

[I]t can be difficult to manage state dependencies that cut across the structure of the component hierarchy in a way that doesn’t introduce a lot of unnecessary complexity or inefficiency.

The truth is that this can be difficult if you use the wrong architecture, as we discussed in the previous installments. But it doesn’t have to be that difficult at all. With the top-heavy architecture, we’ve already dealt with a lot of the potential complexity problems, at the potential cost of performance. Now we just have to find a way to deal with performance problems when they crop up.

One way to think of the problem we were discussing in the previous installment is in terms of the (admittedly somewhat strained) metaphor of articulation points. An arm, for example, is articulated in the sense that it’s got an articulation point (i.e. a joint) so one end can be moved independently of the rest of the body.

The top-heavy architecture only offers a single point of articulation at the very top of the hierarchy, and nothing can move (i.e. update) independently of that. What we want is an articulated architecture, which is to say we want to introduce articulation points into the hierarchy that let its limbs move independently when they need to. This should give us much finer grained control over what re-renders when, but without re-introducing the architectural chaos of the naive hierarchical pattern.

Articulation is about letting the store automatically publish state to the particular parts of the hierarchy that need it

This can be done by taking a declarative approach to information publication, e.g by:

  1. Giving components a simple way of declaring that they have a certain state dependency that may update independently of the component’s ancestors
  2. Giving the store a simple way of declaring when bits of state that it manages are updated
  3. Automatically matching state updates to dependent component updates

The articulated architecture built on these principles is an enhanced version of the top-heavy architecture, where state is managed and distributed from the top, but updates can be automatically published to arbitrary points within the component hierarchy based on declared state dependencies.

There are a number of ways to build architectures that are appropriately articulated. It’s hard to do in a lot of top-heavy frameworks — Redux, for example, seems to allow a limited form of articulation using connect, but the common practice of only using a single-store can make certain things very difficult. Libraries like RxJS and MobX offer reactive programming tools that mesh well with architectural articulation, albeit at a cost: RxJS has a lot of conceptual overhead associated with switching to an FRP streams/observables/etc. style, and the amount of reflection/introspection black magic that MobX does behind the scenes is unsettling. Although I don’t know much about it at the time of writing, a reader points out that the clojurescript framework Om Next is built on similar ideas and I agree that it looks promising.

In the future, I may cover how to put together an articulated architecture using some of the libraries I’ve just mentioned. For the moment, I’m going to cover a much more lightweight option that can be built with the barest minimum of overhead using a tiny library I wrote called react-zine.

react-zine is so named because is built around an extremely small, but non-traditional publisher called zine. Zine is a tiny pub/sub system with a simple twist (the “subjects” of published messages are particular objects, rather than value types like strings) and it provides a means of effortlessly defining reactive components that can serve as points of articulation.

I’m going to introduce react-zine by showing how to build a top-heavy system with it and then I’ll demonstrate how easy it is to introduce articulation points.

Building a Top-Heavy Architecture with react-zine

There are two things that react-zine provides that we’ll use in this article. The first is the publish function (exported directly from zine). The trick is to call publish on anything that needs to update. zine has a corresponding subscribe function, but we won’t need to call that directly because the second thing that react-zine provides is a simple component called Connector, the purpose of which is to connect components to their information dependencies and update them when those dependencies are updated. Connector externalizes state so it can be managed outside of the component and passed in as props.

At its simplest, all Connector needs is a source prop (an arbitrary object) and a render prop (a function). What it renders to the DOM is the result of passing the source prop as the first argument to the render function. (Connector can also take a passProps prop, which it passes as the second argument to the render function.)

Connector subscribes to its source prop and whenever anything publishes that, Connector re-renders.

So suppose we have a component Thing. Perhaps it renders a title and an array of sub-items. Written as a pure functional component, it might look like this:

const Thing = ({title, items}) => (
<div className='some-component'>
<h1>{title}</h1>
<div className='items'>
{items.map(renderItem)}
</div>
</div>
);

Assume renderItem is a function that renders items. Previously we might have provided data to Thing through a store, and rendered it like this:

<Thing title={StoreInstance.title} items={StoreInstance.items} />

To create an articulation point, we just wrap Thing in a Connector and hook it up to StoreInstance:

<Connector source={StoreInstance} render={Thing} />

Now if we want to add an item to the store:

StoreInstance.items.push(newItem);
publish(StoreInstance);

…and the connected Thing will re-render in place.

When Connector mounts, it’ll automatically subscribe to whatever gets passed in through the source prop, which in this case is our store instance StoreInstance. Thus, whenever we call publish(StoreInstance), Connector will automatically re-render Thing with the updated store. If the source prop happens to change, Connector will automatically unsubscribe from the old source, subscribe to the new one and re-render.

That was incredibly easy, as it should be. All it takes to create a top-heavy architecture is to wrap the top-level component with a Connector— this creates a single articulation point at the top of the component hierarchy.

Now suppose somewhere along the way, we’re running into performance issues and we want to introduce further articulation points. How much extra work is that? For the answer, please refer to the title of the next section.

Articulation Points With Basically No Extra Work

What we want is to articulate these list items so they can update independently of their parent component.

We’re going to do this in two simple steps. The first step is to update the publishing scheme.

In a top down architecture, every time we update anything in the store (including an individual item) we call publish(StoreInstance). To introduce new articulation points, you simply publish at a finer grained level. For instance, if item is an object in StoreInstance.items and it updates, we can simply call publish(item) instead of publishing the whole StoreInstance.

Once the publishing scheme is updated, the second step is to update the view layer to react when individual items are published. This is a two-line change.

In the previous section, the items in our list were rendered by the renderItem function. Let’s make a new function to wrap renderItem in a Connector.

const renderReactiveItem = (item) => <Connector source={item} render={renderItem} />;

…and then change the Thing component, to use renderReactiveItem instead of renderItem:

const Thing = ({title, items}) => (
<div className='some-component'>
<h1>{title}</h1>
<div className='items'>
{items.map(renderReactiveItem)}
</div>
</div>
);

That’s it, we’re done.

Now we can update a single item in a gigantic list without re-rendering (or even calling shouldComponentUpdate on every item in) the entire list. If some other reactive component on a different part of the page also depends on the state of an item, calling zine.publish(item) will update them both, independently of any of their ancestors — so we’ve dealt with cross cutting state dependencies and performance issues in one fell swoop. We refresh exactly what needs to be refreshed at a minimal cost, by automatically injecting state changes into the hierarchy at the appropriate places.

The ease with which we’ve solved this problem may belie the complexity we’re managing here — front-end developers have wasted countless hours on more complex and less performant ways to solve problems like this, or settled for needlessly slow interfaces.

One Gotcha You Might Not Have Noticed

One thing worth noticing is that in the above examples, we published state containing objects whenever they’re mutated and we want to push those changes to the interface. This pattern where we put our state in a mutable container object and publish that container is an artifact of how zine works.

If you’ve built a stateful React component, you’re already used to using a container object because that’s exactly what this.state is. Since in top-heavy and articulated architectures, we’re not keeping the state inside our component instances, we can’t just refer to this.state. So we need a stable reference to a particular object in order to coordinate the the publish and subscribe functions.

What this means, if you’re reading between the lines, is that you can’t directly subscribe to value types. We assumed item was an object above, which is fine because objects are reference types. But if it were an immutable value type like a single integer or a string, we wouldn’t be able to directly subscribe to it. Instead, we’d have to wrap that value in a container object that we can reliably reference and subscribe to that.

If you have an array that you’re going to treat as an immutable value type by modifying it functionally in a way that creates a new array (e.g. using map or filter), you might also want to wrap that array in a container object.

It’s often convenient to make state container objects self-managing, which is to say you should feel free to make them regular OOP objects with their own methods and mutable internal state, then have them self-publish by calling zine.publish(this).

A side effect of this way of using zine is that it actually promotes a certain kind of local mutability. There’s a sense in which an articulation point is always built around some source of local state — the whole point of articulation is to contain model-side state changes so they don’t affect more of the view than necessary. So the mutability that comes in does so only at exactly the points of our information hierarchy that are likely to actually need to change. Some readers may be concerned because distributing data through a hierarchy of mutable containers sort of flies in the face of a lot of advice that’s typically given about how to work with React in other contexts where we’re encouraged to use only immutable data and flatten our data hierarchies.

The truth is that a big reason that things like total immutability and hierarchy flattening are encouraged in other contexts is just that those practices make it easier to work with certain frameworks (e.g. Redux, and to a lesser extent, React itself) that normally rely on certain assumptions about how information is published to gate re-renders. The articulated architecture cuts through a lot of those assumptions by publishing updates in a different way, so those concerns aren’t as relevant. In other contexts (for instance, multi-threaded environments with shared state) the assumption of having stable references makes less sense.

That said, there’s no reason you can’t use immutable data types with zine or react-zine, provided you maintain some stable references to publish and subscribe to.

Architectural Patterns: Summary & Conclusion

Now that we’ve reviewed those three architectural patterns and seen the issues associated with them, we can wrap up the central discussion of architectural patterns with a simple three-point strategy for structuring your React application:

  1. If you’re writing a truly self-contained component or an app with a particularly simple information architecture, feel free to start out by keeping state in whatever component makes sense and managing it with this.setState (this is the naive hierarchical architecture)
  2. If you encounter cross-cutting state dependencies, migrate as much state as possible to the top of the component hierarchy and pass it down through props (this is the top-heavy architecture), optionally using pure components to boost performance
  3. If you still have performance or complexity issues, consider selectively introducing articulation points where components can re-render independently of their ancestors in response to outside events (this is the articulated architecture)

That’s the gist of it.

Final Note: Flux and zine

The articulated architecture built on zine is in some ways very similar to Flux. They both have a concept of a store that typically exists outside of the component hierarchy, and they both make use of a kind of pub/sub system that also exists outside of the component hierarchy and is used to coordinate messages between components and stores.

But as we’ve seen, in some ways they’re very different. Flux is concerned with how information flows from components to the store(s) — the idea is to use actions to avoid the complexity associated with propagating information about events the wrong way back up the component hierarchy. The dispatcher always dispatches actions from components to stores. The articulated architecture is concerned with how information flows from the store(s) down to components. It’s designed to avoid always propagating all updates through parts of the hierarchy that needn’t be concerned with them by giving the store(s) the option of publishing state updates directly to any components that might depend on them. zine’s publisher typically dispatches updates from stores to components.

Articulated architecture vs. Flux: Whenever one diagram is the same as another but with the arrows turned backwards, a mathematician gets excited

There’s a sense in which they’re opposites, or dual to one another, to use mathematical parlance.

Since the concerns of the Flux architecture and the articulated architecture are mostly orthogonal, there’s no reason we can’t combine them. The similarities between them make this pretty easy. Here’s one way to implement a dispatcher using zine:

function dispatch (action) {
zine.publish(dispatch, action);
}

We’re making use of a couple little tricks here. First, it provides a (normally unused) optional second argument to zine.publish, which allows us to send payloads to callbacks. Second, dispatch references itself as a subject for published messages — really any object with a stable identity would be fine, but the combination of those tricks allows us to register a store with the dispatcher like this:

zine.subscribe(dispatch, storeCallback);

And then any time anything calls dispatch(action), storeCallback will be called with the supplied action and it can do whatever it would with that action in a normal Flux implementation.

Thus you can build Flux-y stores in basically exactly the same way that you normally would. Creating actions to dispatch is up to you, but there’s basically no difference between the way it’s typically done and the way you’d do it here.

What’s Next?

Nothing! That’s it.

One thing I haven’t addressed at all is communicating with the back end. Choices made on this front have have certain architectural consequences, and there are some libraries coming out that provide a more declarative approach to data fetching, e.g. Relay and Falcor.

That would be a good topic for a future article, but for now this is the end of the series. I hope it’s been enjoyable and informative!

Links To All Chapters

  1. Information Architecture in React and the Naive Hierarchical Pattern
  2. The Top-Heavy Architecture, Flux and Performance
  3. Articulation Points, react-zine, An Overall Strategy and Flux-duality

--

--

Skyler Nelson

I’m a cognitive science nerd with a graduate degree in philosophy of physics who makes user interfaces for an A.I. company, for some reason.