Understanding State Management

Julien Etienne
7 min readFeb 1, 2024

--

The topic of state is something that is rarely discussed meaningfully in software engineering. In one way or another, we will all be faced with learning some kind of state management.

There are almost no formal teachings of the fundamentals of state management (not state machines in particular) because the fundamentals begin outside of the software realm, within the physical world.

It helps to think of state management as a camera recording activities and changes within a scene, or perhaps multiple cameras working in unison.

Cameras observe, they don’t have layers. I only use this analogy because it debunks the notion of nested state, which is just something fabricated by developers who find joy in over-engineering simple things.

There’s a powerful design pattern called the Publish–subscribe Pattern or pub/sub for short. It’s commonly used in Golang (Not to be mistaken for GCP’s PubSub, but yes that is also somewhat a type of pub/sub)

All general purpose and UI state management systems:

  • JS: Redux/ Vuex/ MobX/ RxJS/ XState/ NgRx, React Hooks, etc
  • Flutter: Provider/ Bloc/ Riverpod etc
  • Rust: Loro
  • Java: Simple-Stack
  • Kotlin: PreCompose

Essentially, lead to the same outcome as the pub/sub pattern. To be honest, the majority of applications seldom require anything beyond pub/sub.

An imaginary programming language for demonstration purposes:

fn FnToCallWhenDoorChangesState(state: string) void {
if (state == "opened"){
// Do something
} else if (state == "closed") {
// Do someething else
} else {
// Manage pending state
}
}
// When "door" changes call fnToCallWhenDoorChangesState
sm.SubscribeTo("door", fnToCallWhenDoorChangesState)
// Notify the SM system when the door opens
sm.Publish("door", "opened")

You don’t necessarily need a pending state, in many case it’s overkill but you can sometimes reduce edge cases by conceptualizing intermediaries as physical entities.

Pub/Sub

Pub/sub state management typically has at least 3 main functions, similar to the below, but there are many variants.

  • Publish(name, state);
  • Subscribe(name, ref, callback);
  • Unsubscribe(ref);

The entire system can be written in around 30 to 150 lines of code on average in your preferred programming language. This is enough to power state management for an entire application as large as something like Twitter or Pinterest on any platform or device.

Do I need to build a pub/sub?

It’s really up to you. I would recommend building one for learning purposes to help comprehend the simplicity of managing general purpose state vs the perception of state management from the tech-influencer world. They are fairly easy to build, the concept is just as you imagine.

Abusing State Management

It makes no difference how great of a programmer you are, if you over-complicate state management it will backfire in edge-case bugs that you may or may never personally encounter.

I love Spotify, they have made some great engineering decisions, but unfortunately the state management on the web player has been comically poor for almost two decades.

How bad? Randomly playing, stopping and skipping for years. In all fairness they have improved over the years but there are still many noticeable bugs that persist.

Rule of thumb: Choose the simplest solution that meets your needs. Don’t try to show off

How to choose a State Management Solution

What does my app need to do?

Planning is the first step. Find out as many requirements as possible then decide if you need anything more than generic state management. Resist the urge to lie to yourself, always try to keep things simple.

What features are unnecessary or overkill

This part is also pertains to state-management library authors as well as your every day consumer deciding what is relevant.

  • Nested state management: There’s no such thing, we are typically rendering boxes not solving quantum computing challenges, so please behave yourself.
  • Future/ Promise based state management: It may sound appealing, but using an SM library to “implicitly” handle atomicity-like/ mutex-like behaviour is not advisable, as low powered devices may result in noticeable performance penalty due to additional operations being incured. It’s basic maths: You have 50 subscriptions. 10 depend on a future/promise, you’ve now slowed down 40 without good reason. Manual async programming typically gives you more control and provides good separation of concerns.
  • Reducer based state management: Let’s not pussyfoot around, this part is about Flux, Redux and Redux influenced systems. A SM library should only be responsible for i) What it stores and ii) What it retrieves. SM libs are either immutable or explicitly allow pointers and references by design.

The immutable, unidirectional, pure-function mumbo-jumbo was never necessary. E.g. An app made with Redux or Flux results in the same thing as an app made with React Hooks. A reducer has no control on the immutability of application logic and it shouldn’t.

As a fish, it’s about time you stopped purchasing water, when you live in the bloody ocean!

“But my app is Large”

A small website blog could potentially have far more state management “requirements” than a fortune 500 company’s flagship app. Choosing is more about feature requirements than codebase, experience or team size.

For more specific requirements

Below are some features that I believe to be significant when considering advanced user navigation, animations, desktop-like applications and time-travel (history).

  • Persistent State: Do you need to persist state, e.g. undo/ redo
  • Trajectories: Animations can benefit from the concept of going from A → D → B → C. Persistent data allows this to be reversed
  • Directional functions: This is really just something I’ve coined, I prefer the term reversible functions because it indicates that the function is the reverse behavior of the forward direction. Your state machine has to understand what is forwards and what is backwards. If A → B → C is forwards then C → A is backwards and will call the function that represents the direction. This is useful for gaming and animation, to be frank I have no idea if anyone is using this it’s just something I have personally implemented.
  • Persistent storage: Not to be confused with persistent state. Persistent storage doesn't necessarily require the state to be persistent its just the concept of the current state or persisted state being stored locally on the device. This is usually best integrated with a local database such as ObjectBox in Flutter or IndexedDB on the web.

Don’t get too bogged down on terminologies, they will vary between platforms and libriaries.

Relevant but not a necessity

  • Finite state: Can be very useful in the right circumstances, for steps that follow a continuous trajectory or finite loop. Don’t be overwhelmed it’s nothing but dataset traversing. You’ve probably built a few unknowingly via sets, tuples, arrays, slices, lists, maps or objects.
  • Hooks: A subscription is technically a hook and holistically speaking you really only need one hook, though when in Rome (E.g. React) you may have no choice but to do what the Romans do. React has decided to intertwine framework lifecycle hooks into your application’s state management. Hooking has been around since before the 1960s, it can be very useful. However SM systems are supposed to help you build life-cycles, rather than introduce a life-cycle its self…

(I’m looking at you: useEffect, useMemo, …componentDidMount 👀)

When not to use State Management

Don’t use state management in places where a function call can suffices. If the effects of a process are knock-on (consecutive) or inevitable, and you can easily perform your intentions with a function call somehow, don’t abuse state management.

The Dark Ranty Conclusion

If a state-management system introduces too many complexities or rigidness, you will spend far more time solving missed edge cases. STATE MANAGEMENT IS ALL ABOUT EDGE CASES.

We are building software for humans so most edge-cases are not black and white, they are as analogue and abstract as our human behaviours and expectations.

If you make the difficult stuff easy to work with, you spend less time fighting before your stand-up meeting.

THUS

Allowing you to focus on more interesting things like improving user experience, and other cool features that have been sitting in backlog for ever.

Every time you over-engineer you are hurting your own developer experience. And over a long period of time, you will evolve into a creature who is brilliant at dealing with over-complicated garbage.

Your brain turns into melodic mush, competent at fusing warm trash with hotter trash, though utterly oblivious and confused at why anyone would want to waste time building efficient software or writing stupid Medium articles.

Yes, it feels like the scene from Labyrinth when navigating all that technical debt

— Julien

--

--