I spent three years of my career treating Redux and Zustand like they were the default way to build any app. If a piece of data needed to be shared between two components, my first instinct was to hoist it to a global store. I thought I was building for scale. In reality, I was just creating a giant, tangled web of dependencies that made debugging a nightmare.
The Global Store Trap
The problem starts when we confuse "shared state" with "global state." Not every piece of data that is used in multiple places belongs in a global store. When you put everything in one place, you lose the ability to reason about your components in isolation. You end up with components that cannot function without a specific provider wrapping them, making testing and re-use much harder.
I remember a project where I put the user's form input state in a global store. Every single keystroke triggered a global state update, which triggered a re-render of the entire page layout. I spent two days optimizing memoization just to fix a problem I created by putting the state in the wrong place.
The Hierarchy of State
To avoid this, I now follow a strict hierarchy when deciding where data should live. I ask these questions in order:
- Can this state live in the component that uses it? (Local State)
- Can I lift the state up to the nearest common parent? (Lifted State)
- Is this data actually a server cache? (Server State)
- Does this data truly need to be accessible from any page in the app? (Global State)
Handling Server State Separately
One of the biggest mistakes we make is mixing server data (API responses) with UI state (is the sidebar open?).
Server data is not actually "state" in the traditional sense. It is a cache of what is currently on the server. When you treat API data as global state, you spend half your time writing boilerplate to handle loading spinners and error states across different slices of your store.
Using a dedicated tool for server caching lets you remove about 60 percent of your global state code. You get automatic caching and re-fetching without having to manually dispatch actions to update a store.
When to Actually Use Global State
Global state is for things that are truly universal. Examples include:
- User authentication status
- Theme preferences (dark mode vs light mode)
- Language settings
If you are using global state to pass a user ID through five layers of components, you are just using a global store to avoid prop drilling. While prop drilling feels annoying, it is often more honest. It tells you that your component tree is too deep or that your components are too tightly coupled.
A Practical Strategy for Refactoring
If you realize your store has become a dumping ground, do not try to rewrite it in one go. Instead, try these steps:
First, identify any state that is only used by one feature. Move that into a local state or a feature-specific context provider.
Second, separate your API calls. Move the logic for fetching and caching out of your global store and into a dedicated hook or library.
Third, look at your props. If you see a prop being passed through a component that does not use it, ask if that component should be broken down further or if the state belongs in a more logical parent.
The Takeaway
Complexity is a cost. Every line of state management code you write is a line you have to maintain and debug. The most scalable architecture is often the one with the least amount of moving parts.
Stop reaching for the global store by default. Start with local state, lift it up only when necessary, and treat your server cache as something entirely different from your UI state. Your future self will thank you when you can delete a feature and only have to delete one folder, rather than hunting for state slices scattered across your entire project.
Top comments (0)