DEV Community

Ntty
Ntty

Posted on

Stop Over-Engineering Your State Management

The Trap of the Global Store

I spent three years of my career thinking that every single piece of data in a web application needed to live in a global store. Whether it was a user profile or whether a dropdown menu was open, it all went into Redux or Vuex. I thought I was being organized. In reality, I was creating a maintenance nightmare.

When you put everything in a global store, you create invisible dependencies. You change a value in one place, and a component three levels deep in a different branch of the UI tree re-renders for no reason. You end up spending more time debugging the state flow than actually building features.

The State Hierarchy Rule

To avoid this, I started following a simple rule: keep state as close to where it is used as possible.

1. Local Component State

If a piece of data is only used by one component (or its immediate child), use local state. This includes toggle switches, form input values before submission, and hover states. If you move a 'isModalOpen' boolean to a global store, you have to remember to manually reset it when the component unmounts, or the modal will pop up unexpectedly the next time the user visits that page.

2. Lifted State

If two sibling components need the same data, lift the state to their common parent. Pass the data down via props and pass a callback function to update it. It feels like 'prop drilling' at first, but for 2 or 3 levels, it is far more explicit and easier to trace than a global dispatcher.

3. Context or Provider Pattern

Use this for truly global data that rarely changes. Think of themes (dark/light mode), authenticated user info, or localization settings. Context is great for avoiding prop drilling, but it is not a replacement for a state management library because it is not optimized for high-frequency updates. If you put a fast-changing timer in a Context provider, you will likely trigger a re-render of your entire app every second.

4. External State Libraries

Only reach for a dedicated store (Zustand, Redux, Pinia) when you have complex data dependencies. For example, a collaborative text editor or a complex dashboard where an action in the sidebar must instantly update a chart in the main view and a notification in the header.

Dealing with Server State

One of the biggest mistakes I made was treating server data (API responses) the same as UI state.

Storing API data in a global store usually leads to 'stale data' bugs. You fetch a user list, store it in Redux, and then update a user on the server. Now your global store is out of sync.

Instead, use a caching layer or a data-fetching hook. Let a dedicated tool handle the loading states, error handling, and cache invalidation. Your global store should only hold the minimal amount of data needed to coordinate the UI, not a mirror of your entire database.

The 'Delete' Test

Whenever I am tempted to add a new slice to a global store, I ask myself: 'If I deleted this component, would any other part of the app break?'

If the answer is no, the state belongs in the component. If the answer is yes, I check if it can be handled by a simple Provider. If it is still too complex, only then does it go into the global store.

Concrete Takeaway

Stop starting your projects with a global store by default. Start with local state. Lift it up when it becomes painful. Move it to a Provider when it becomes repetitive. Only implement a full state management library when you can point to a specific, complex data flow that cannot be solved with the built-in tools of your framework.

Less code in your store means fewer bugs and a faster app.

Top comments (0)