Frontend State Management is a Skill Issue
I’ve seen some horrifying frontend codebases in my career. Not horrifying because of complexity in the problem domain, but horrifying because of manufactured complexity that did not need to exist. State management is the usual culprit.
The spectrum of frontend state management goes something like this: on one end you have developers struggling with React hooks and context, getting frustrated because there are no guardrails stopping them from shooting themselves in the foot. On the other end you have developers who overcorrected so hard they built their own state machines with controllers and managers and services and god knows what else. Both extremes are missing the point entirely.
The React Hooks Problem
React hooks and context are powerful but they do not prevent bad patterns. You CAN prop drill through seventeen components. You CAN create context providers that cause your entire app to re-render on every keystroke. You CAN create circular dependencies between hooks that make debugging a nightmare. React will not stop you. This frustrates people who came from frameworks with more opinions baked in.
But the solution is not to reach for a state management library or build your own. The solution is code reviews and established patterns in your codebase. If your team keeps falling into the same traps, document the traps and review for them. This can be fixed by processes instead of refactoring your whole app out of react.
The Overcorrection
Here’s where things get really painful. Some teams look at the chaos of unstructured hooks and decide they need “proper architecture.” They build elaborate state managers with actions and reducers and selectors and middleware. They create controller classes that orchestrate complex state transitions. They draw diagrams of data flow that look like subway maps.
This is way too much complexity for something that should be simple. If you need a manual state manager and controller to handle complex logic on your frontend, you have already done something wrong. You have put business logic where it does not belong.
Frontend Should Not Think This Hard
Complex business logic belongs on the backend. Full stop. Your frontend should only care about three things:
- When to fetch data
- When to invalidate data
- When to render data
That is it. That is the whole job. Your frontend is a display layer, not a business logic layer. The state of your application should live in your backend and your frontend should just be asking for it and showing it.
I do not care what communication strategy you use. REST, GraphQL, websockets, real-time APIs, Electric SQL, whatever. The principle is the same: your data IS your state. Stop manufacturing state on the frontend when it should be coming from your database.
The Optimistic Update Trap
Speaking of manufactured complexity, let’s talk about optimistic updates. The idea sounds great on paper: update the UI immediately while the request happens in the background. Users get instant feedback. Everything feels snappy.
In practice? Optimistic updates are a massive development effort to do correctly. What happens when the request fails? What happens when two optimistic updates conflict? What happens when the user navigates away? What happens when they refresh? Every optimistic update is a potential inconsistency between what the user sees and what is actually true.
The industry has trended toward “make everything optimistic” because it feels slightly better. But does it actually feel that much better? Does EVERY action in your app need to be optimistic? Is shaving 200ms off a button click worth the engineering complexity of maintaining two versions of reality?
I would argue no. Most of the time, no.
Recognizing which parts of your app actually benefit from optimistic updates and which parts can just show a loading spinner requires experienced engineering judgment. This is not a simple question with a simple answer. It depends on the specific interaction, the failure modes, the user expectations. It requires thinking.
Keep It Simple
Here’s my take: keep your data as your state for as long as humanly possible. When your only concerns are “when should I fetch” and “when should I refetch,” your code stays clean. You do not need complex state machines. You do not need elaborate synchronization logic. You just need a good data fetching library and a clear understanding of your cache invalidation strategy.
This is an architectural decision. It should be thought through carefully at the start of a project, not discovered painfully six months in when your state management code is an unmaintainable mess.
Build display layers that display. Let your backend handle the thinking.