Why State Management is All Wrong

A Better, Easier, More Natural Way to Achieve State

Brad Lemley
12 min readMay 31, 2019
Rube Goldberg Machine: a collection of clever gadgets connected in convoluted ways to perform a simple task

State management is in a bad state: actions, reducers, stores, middleware, this hook, that hook — it’s all so unnecessarily complicated, and it slows application development. Why is it necessary to jump through so many hoops just to implement simple functionality? What exactly are we trying to accomplish, anyway?

In this article, we’ll uncover the architectural ambiguity that lead to these unnecessarily complicated “state management” solutions. Clarifying this ambiguity leads to a much simpler and more natural solution — a solution that empowers us to develop higher-quality applications faster.

This article walks thru the design process leading to Stated Libraries, an alternative to state management.

Understanding State Management

To fix any problem, we first need to understand it. To understand state management, we need to look at the view framework architecture. State management is one of only two parts in the view framework architecture:

View Framework Architecture

Haven’t seen this diagram before? Me neither, but there’s a similar diagram in a flux presentation with the note: our ideal view of the world looks like this. In that presentation, the box is labeled “data” instead of “state management”. And therein lies the problem — what exactly is that box? After some pondering, I came to realize:

The box represents the entirety of application logic and functionality.

Perhaps you already know this, perhaps some part of me already knew this — but bringing this to the conscious, putting it on paper, changes something. It provides a much clearer picture of the architecture:

View Framework Architecture

For me, this diagram is the key to understanding the architecture and understanding why “state management” solutions are so convoluted and weird: they focus on managing state instead of implementing functionality. The “state management” approach is backwards and fundamentally flawed: functionality produces state — not the other way around.

This focus on managing state is the reason Redux requires middleware to perform simple asynchronous operations — it tries to squeeze functionality into state…but, it doesn’t fit…so, it resorts to clever gadgets to achieve functionality indirectly around state.

Yes, there is a better way…

But, before we go on, there’s one more very important idea to take away from this architecture diagram — functionality is independent of view. That means application functionality could be implemented — and tested — independently of any view or view framework. That makes sense, there’s no reason application logic should be tied to a view framework whose job is simply to render a view. So — think again if you really want to implement application functionality with React hooks…not only will it be unnecessarily complicated, but any functionality you implement with hooks will be unnecessarily coupled to the React framework.

Implementing Functionality

Let’s start with a standard “state management” example, the counter, and just implement counter functionality:

This counter example is implemented with a closure variable and a getter, but it could be written as an ES6 class, or some other technique, it doesn’t matter. The point is: we know how to implement counter functionality and it naturally and necessarily maintains state.

Let’s try another standard state management example, the todo library:

Again, this library uses closure variables for state: todos and isFetching. Maybe you’ve written libraries like this? — using a variable or object property for each piece of state. This is just standard implementation of functionality, this isn’t even unique to javascript.

We can see the concept emerging in these two examples: state is a natural part of functionality — they are two parts of the same thing, like yin and yang. Why would we want to jump through hoops to implement functionality in a state management framework when functionality naturally produces state?

Side Effects

You might have noticed that fetchTodos is asynchronous. There’s no fanfare, no middleware — just regular old asynchronous functionality. Side effects are a normal part of functionality. If you’ve worked with Redux, you’ve seen the flip side — asynchronous functionality and side effects don’t fit nicely into state, so they must be bolted on to the system through add-ons, working around the core state system.

Aha! you’re thinking —but, if there are side effects, then the system is not pure and if one cannot replay the same actions to achieve the same state…well…it will be chaos! Well, I have some sad news for you: real apps do cause side effects. In real apps, replaying actions won’t reproduce the same state — you can’t replay actions in banking app, nor in a social media app, and expect to get the same results.

So, Redux’s focus on pure state reducers and pure functions — and the ability to replay actions with the same result — sounds clever, but is misguided. The whole Redux architecture is based around this concept that relegates real-world functionality (side effects) to convoluted gadgets (middleware), just to enable some functionality that isn’t practical anyways. See the problem?

Standardized State

For the next step in our implementations, let’s find the commonality between the counter and todo library examples and standardize the output to be state, like in the architecture diagram.

The libraries in this form closely resemble the architecture diagram. We’re developing a pattern that bridges the gap between state and functionality — this pattern can be used to implement any functionality.

Note: The example above mutates state for continuity with the previous example. Real Stated Libraries use immutable state, and it will be introduced in subsequent examples, but discussion of immutable state is beyond the scope of this article. See Redux docs for excellent info on immutable state.

State Notifications

The architecture diagram shows state as an output and the implementations have a state property — but we really need the ability to notify the view when state changes so the view can update. Such notification can be handled by simple pub/sub (publisher/subscriber), so let’s update the diagram to indicate that state output is accomplished via pub/sub:

View Framework Architecture with Pub/Sub State

There are lots of ways to implement pub/sub — addListener, subscribe, etc. — there’s lots prior art to leverage. We’re going to use an observable. Observables are usually associated with reactive programming, and we’ll get to that later; but for our current purposes, observables are just generic objects that support pub/sub for pushing data to subscribers, and we can use them as an off-the-shelf pub/sub solution for pushing state. There is a naming convention for observables — post-fix variable names with “$” — so, the state observable will be state$.

These implementations are very similar to what we started with, but they now support a standard pub/sub state$. The common functionality can be factored out into a base implementation:

Base Implementation

It’s not totally necessary to use a base object, but when we do, the library implementations become extremely simple:

Counter using Base Implementation
Todo Library using Base Implementation

Derived State

Libraries can support derived state, too. The base implementation can support derived state:

Base Implementation Supporting Derived State

This example adds completedTodos and activeTodos to the todo library’s state and shows how memoization and getters can be used together to make derived state efficient and transparent — memoization makes the calculation efficient and the getter allows it to be accessed via state.completedTodos instead of state.completedTodos(). This example uses reselect for memoization, a library commonly used with Redux. There is no magic, this is just regular vanilla javascript.

Todo Library with Derived State

Testing

These functionality libraries are not only easy to develop — you basically already know how — since they are self-contained objects, they are easy to test, too. A typical test invokes library methods and verifies the resulting state. Here’s an example todo library test:

So, these areStated Libraries: completely self-contained javascript objects that output state via pub/sub — easy to implement, easy to test.

But, how does it all fit together?…

State Composition

Since functionality and view are independent entities, the output state of the functionality will not exactly match the input requirements of the view. A real application will be composed of multiple functionality modules and multiple view modules. The state composition layer transforms and combines state and is the key to making all of these pieces fit together. The state composition layer sits between the functionality and view:

State Composition

Conceptually, this state composition object operates on each state as it passes through — receiving a state, transforming it, and emitting the transformed state — it’s called a state operator.

Adding a second independent functionality module, the state operator combines the states and emits a combined & transformed state every time it receives a state from either functionality module. The output is the same as if there was only one functionality module.

Multiple Functionality Modules

To support multiple view modules, a state operator can be used to compose state for each view module:

Multiple View Modules

Finally, since both the input and output of a state operator is observable state, state operators can be chained together:

State Composition

State composition is a powerful layer of logic between the functionality and view. It might be difficult to fathom without concrete use cases, but conceptually, state operators can do just about anything with state. They can remember previous states. State operators don’t even have to emit a state each time they receive one — a state operator could compare a previously calculated state output against a new one and not emit if they are the same.

Implementing State Composition

There’s an entire branch of software engineering dedicated to this type of data composition functionality: reactive programming. Reactive programming is the practice of operating on pushed data — in our case, the pushed data is state.

Observables are reactive programming objects that provide data via pub/sub. Reactive programming libraries, like RxJS for javascript, support generic operators which operate on the data passing through observables; for example, map transforms data, and combineLatest combines data from multiple source observables into a single observable output. These generic operators can be chained together to do just about anything — often achieving very complex functionality out of just a few simple generic operators.

These generic operators can be used to compose state as shown in the diagrams above — however, we’re going create a our own custom operator, mapState, that supports all of the state composition functionality we need in just one operator. mapState is so easy to use that you won’t even know you’re reactive programming. It is similar to mapping items in an array, except that the transform function is called every time a state is received. Like all reactive operators, the output of mapState is an observable. Here’s an example:

State Composition with mapState

You probably noticed that, in addition to reshaping the todo library state, the transformer function adds the addTodo method to state. Methods and functions can be treated like regular pieces of data and can be mixed into state. Of course, adding methods to state this ways requires the methods to pre-bound where needed; i.e., if the method uses this, it needs to be pre-bound.

The second form of mapState supports multiple inputs, an array of observables. The transformer function then takes an array of states, one for each input observable, and is called every time any of the inputs emits a state. The object stores the last state received from each input observable and calls the transformer function with all of the “current” states.

State Composition with mapState

Let’s take a look at a more advanced example. This example creates a visibleTodos$ observable using state from a todo library and a visibility library and then uses visibleTodos$ as input to mapState to create other observables.

Advanced State Composition with mapState

Testing State Composition

The state composition layer can include computations, derivations, memoization, etc. All of this state composition logic is still independent of the view or any view framework, and it can be easily tested:

Testing State Composition

Connecting to Views

The final piece of the puzzle is providing state to the view. Everything we’ve done up until now has been generic application functionality and logic — completely independent of the view and view framework. Each view framework has a mechanism, or mechanisms, to provide state to a view and cause it to re-render, so this final link is specific to each view framework. This final link is pretty simple — take an observable and deliver its data (state) to the view. This piece actually is generic, too — these connectors can deliver data from any RxJS-compatible observable, not just a Stated Libraries state observable.

React

For React, a view module is a React component. There several ways to deliver state to React components and cause them to re-render: setState() for class components, useState() for functional components, or props. Any of these mechanisms will work, and Stated Libraries supports all of them.

For this example, we’ll use the connect function which is similar to react-redux connect. It creates an HOC (“container”) to provide state to a wrapped (“presentational”) component as props. Continuing from the last example, a React component using appState$ might look like this:

The HOC adds an extra component in the React component tree. (Note that connect does not use React context, so there is not an additional context consumer component in the tree.) It is possible to eliminate the extra HOC component and inject the observable state directly into the component. For functional components, the use hook accomplishes this:

Functional App component with use hook

Stated Libraries supports a similar option to inject state directly into a class component. In my opinion, both HOC and direct injection methods are perfectly valid — there is no right or wrong way, it’s a personal or team decision which method to use. Perhaps some best practices will emerge.

Other View Frameworks

React is currently the only supported view framework, but it should be pretty easy to add observable bindings for any view framework. In fact, since they are generic bindings for observables, they might already exist. Please PR it or just do it.

Library-to-Library Interactions

There’s another layer of functionality that is often necessary in applications — a layer of logic connecting functionality modules together. This could be considered the business logic of the application. For example, a todo library might need to know when a user logs in; and, likewise, the todo library might need to request a re-login. This type of functionality can be achieved by monitoring a library’s state:

Library-to-Library Interactions

Sometimes the conditions for these interactions can get complicated, requiring many extra variables to remember previous states. Reactive programming can make these types of interactions much easier:

Library-to-Library Interactions with Reactive Programming

We’re not going to go into any detail about reactive programming and how these RxJS operators work, but hopefully it makes some sense and can be a good introduction if you’re not already familiar with reactive programming. Here are a couple notes:

  • Stated Libraries do not have a dependency on RxJS; Stated Libraries' core observable and mapState operator are interoperable with RxJS.
  • Reactive programming/RxJS can be used internally in Stated Library implementations, just like any other side effect. The reactive programming described above is intended for library-to-library interactions —any library-specific functionality should be contained in the library, not implemented as an external add-on monitoring the library’s state.

More Features

Stated Libraries support much more than what’s covered in this article — time-travel debugging, state hydration, and more.

Global vs Local

Stated Libraries initial focus is on global functionality and state because “state management” solutions have typically focused on global state. But, the architecture diagram and concepts — functionality produces state, functionality independent of view — apply to local state as well. Stated Libraries philosophy is that all application functionality and logic can and should be implemented as generic, view-framework-agnostic functionality and that the view layer should be focused only on the view and binding the view to the functionality.

It certainly is possible to use a counter library or todo library locally in a React component — they just need to be tied to the component’s life cycle. However, the real world use cases need to be further explored and developed. For example, can auto-complete functionality be implemented in a framework-agnostic way? Or, can the functionality of downshift be implemented in a generic, view-framework-agnostic way? Can such generic functionality be easily used in view components/frameworks or would it end up being a Rube Goldberg machine?

Final Summary

Current “state management” solutions focus on managing state instead of implementing functionality. This focus on managing state causes a problem because functionality is the goal, and some necessary functionality, like async operations and side effects, don’t fit cleanly into state, thus requiring convoluted workarounds — like weird middleware — to achieve.

A better approach is to focus on implementing functionality which naturally produces state. Stated Libraries focus on functionality and support independent functionality modules.

--

--