All posts
A person studying pages of notes pinned across a wall
Photo by Startup Stock Photos

Posted 5 min read

Why State Management Becomes Hard

State gets hard not when you store it, but when several places change it. One store, one dispatch, one reducer — and every update has a reason you can trace.

programmingsoftware-engineeringjavascriptclean-code
On this page

At first, managing state feels pretty simple — it’s just variables. And honestly, it usually works fine… until it suddenly doesn’t.

As an application grows, and multiple parts of it start changing the same state, it becomes increasingly difficult to follow, trace, and understand what’s actually happening.

The Problem

let count = 0; const incrementCounter = () => count++; const decrementCounter = () => count--; button.addEventListener('click', incrementCounter); otherButton.addEventListener('click', decrementCounter);

Looking at this, multiple parts of the application can change the same state. And at first, this seems fine.

But as the application grows, it becomes harder to track what changed the state and when — which leads to unexpected behaviour, subtle bugs, and a lot of guessing.

The real problem wasn’t updating state. It was knowing what changed it — and when.

Visually, it looks something like this:

[Button A] ──┐ [Button B] ──┼──> [ count ] [Timer] ──┘

State is being changed from multiple places, with no single source of truth for why it changed.

A Different Way to Think About State

I started wondering — what if every change had to go through a single place?

That way:

This is where I started experimenting with centralising state into a single system.

A Simple Store

const store = new MockState(reducer, initialState); store.dispatch(action);

Instead of updating variables directly, everything flows through the store.

There’s now a single place where state lives — instead of being scattered around the app. And crucially, nothing can change state without going through it.

See It in Action

The UI is just a reflection of state — and you can see that directly in the store panel. Try updating the name or the count, and watch how every change flows through the same path.

Before vs After

Before this, state could change from anywhere. After this, every change has a clear, traceable path.

Before:

[Button A] ──> count [Button B] ──> count [Timer] ──> count

After:

[User Action] ↓ dispatch() ↓ reducer() ↓ state ↓ subscribers (UI updates)

Every change now follows the same predictable path — and that changes everything about how easy the code is to reason about.

What Is a Reducer?

A reducer is just a function that takes the current state and an action, and returns the next state.

function reducer(state, action) { switch (action.type) { case "increment": return { ...state, count: state.count + 1 }; default: return state; } }

An action is simply an object that describes what happened:

{ type: "increment" }

Instead of mutating state directly, the reducer returns a new version of it. This makes every update explicit — you can always look at an action and know exactly what it will do.

What This Structure Gives You

At this point, it might feel like a lot of structure for something that used to be simple. But each piece earns its place:

Reducer — defines how state changes in a predictable, testable way.

Dispatch — a single entry point for all state updates. Nothing bypasses it.

Subscribe — lets parts of your UI react automatically when state changes.

Middleware — lets you observe or modify actions as they pass through. For example, a logger:

const loggerMiddleware = ({ getState }) => (next) => (action) => { console.log("Dispatching:", action); const result = next(action); console.log("Next state:", getState()); return result; };

Visually, middleware sits in the flow like this:

Action → dispatch → [ middleware ] → reducer → state ↓ (logging / debugging)

This made it dramatically easier to see what was happening — something that was almost impossible to do before.

Why This Actually Helps

The important shift here is that state no longer changes directly. Every update is the result of a dispatched action, which means every change has context.

That might sound minor, but in practice it’s the difference between opening DevTools and immediately knowing what happened versus spending an hour adding console.log everywhere trying to figure it out.

Where This Comes From

If this structure feels familiar, it’s because it’s similar to patterns used in tools like Redux or Zustand.

This wasn’t meant to be a full implementation — I was just trying to understand why these patterns exist by building a simplified version myself. And that turned out to be the best way to learn them.

It helped me realise that these tools aren’t really about storing state. They’re about controlling how state changes.

The Trade-offs

This pattern isn’t free — it’s worth being honest about what it costs.

For small applications, this structure can feel like overkill. A counter or a form doesn’t need a reducer. Sometimes a variable really is enough, and reaching for a store adds ceremony without adding clarity.

But the inflection point comes sooner than you’d expect. Once you have more than two or three unrelated pieces of state, more than a handful of components that need to share it, or any real async logic — the structure starts paying for itself. The boilerplate you write upfront is cheaper than the debugging sessions you avoid later.

The question to ask isn’t “is my app complex enough for this?” It’s “am I already spending time trying to figure out what changed my state?” If the answer is yes, the pattern is probably worth it.

Final Takeaway

The issue was never storing data. It was managing how the data changes.

Once those changes become predictable — once every update has a clear path and a clear reason — everything else gets easier. Not just debugging. Reading the code, adding features, handing it to someone else.

State management tools exist because that problem is real. And once you’ve felt it, the structure stops feeling like overhead and starts feeling like relief.


Originally published on Medium .

2026 © Jordan O’Hara