I spent three years of my career reaching for Redux or Zustand the moment a project felt "medium sized." I thought that was what professional developers did. I would set up slices, actions, and complex store configurations before I even had a working UI.
Most of the time, I was solving a problem that did not exist. I was managing server cache in a global store, which led to bugs where the UI showed stale data because I forgot to trigger a fetch on a specific page transition.
The State Confusion
The biggest mistake we make is treating all state as the same. State is not just one big bucket of data. It falls into three distinct categories:
- UI State: Is this sidebar open? Which tab is active? This belongs in the local component.
- Server Cache: What is the user's profile name? What are the items in the cart? This is just a local copy of data that lives in a database.
- Global App State: The current authenticated user or a preferred theme. This is actually very small.
When we put Server Cache into a global store, we create a synchronization nightmare. We end up writing a dozen lines of boilerplate just to update a single string in a database.
The Simple Hierarchy
Instead of jumping to a library, try this hierarchy of decision making.
First, keep it in the component. If only one or two components need the data, use local state. If you are passing props down three levels, that is a smell, but it is often better to just pass the props than to introduce a global store that makes the component non-portable.
Second, use a dedicated server state tool. If you are using React, tools like TanStack Query handle caching, loading states, and re-fetching automatically. They remove the need to store the results of an API call in a global store. The tool becomes the cache, and your components just hook into the data they need.
Third, use Context or a small store for truly global data. Your theme or user session does not change every second. Using a simple provider for this is enough.
A Real Example of Over-Engineering
I once saw a project where the developer used a global store to track the value of a search input field. Every single keystroke triggered a global action, which updated the store, which re-rendered half the application.
It was a disaster for performance. The fix was simple: move the input value to a local useState hook and only trigger the search function when the user hits enter or after a debounce period.
We often confuse "centralized" with "organized." Centralizing everything in one store does not make it organized. It just makes it a single point of failure that is hard to debug.
How to Audit Your Current Project
If you feel your state management is getting bloated, do this audit:
- Look at your global store. For every piece of data, ask: "Does this come from an API?" If yes, move it to a server state manager.
- Ask: "Is this used in more than two distant branches of the component tree?" If no, move it to local state or lift it up one level.
- Ask: "Does this data need to persist across page refreshes?" If yes, it belongs in a cookie or localStorage, not just a JS variable.
The Takeaway
Complexity is a cost. Every line of boilerplate you write is a line you have to maintain. Start with the simplest possible way to move data. Use local state first, server state tools second, and global stores only as a last resort. Your bundle size will be smaller, and your brain will be less stressed.
Top comments (0)