# React State Management Architecture: Start With the Kind of State

> Before asking which library to use, name the kind of state you're managing. A layered approach to state in React, from local state to Zustand.

Source: https://omergulcicek.com/en/blog/react-state-management-architecture

Conversations about state management usually open with "Which library should I use?" The better first question is: **what kind of state am I managing?** Pick a tool before you can answer that, and it adds complexity instead of structure. A sound state strategy starts by naming the kind of state, then leans on what React already gives you, then looks at subscriptions, and only at the end brings in libraries, as an optimization layer.

## 1. The foundation: what kind of state is it?

Every architectural decision rests on this split:

- **UI state:** State that only drives the interface: an open modal, a dropdown, form fields, an accordion. It usually belongs to one component or a small subtree.
- **Server cache:** Data that lives on the server and is kept on the client for fast access: a user profile, lists, API responses. This isn't really state, it's a **cache**. Invalidation, refetching and stale-while-revalidate are problems for a cache layer, not for a state library.
- **Workflow / domain state:** Business processes (draft → submitted → done), multi-step flows, and transitions you need to trace and debug over time.
- **High-frequency interactions:** Drag-and-drop interfaces, live charts, dashboards with many widgets. Here the question isn't just where the data lives, but who subscribes to what, and how often it renders.

![Passing props down the component tree](https://omergulcicek.com/blog/2026/02-01/passing-props.png)

Reaching for a "global store" or "putting everything in Context" before you've sorted state into these four groups is premature. Name the kind of state first; the choice of tool tends to follow on its own.

## 2. The default: React and colocation

React is a state management tool at heart. If your component tree runs from `<App />` down to `<input />`, the core strategy is to keep state **local** and follow **colocation**: keep state next to the code that uses it.

- **Start local:** A modal's state, form values or a component's temporary data should live in one place. Moving data nobody else needs into a global store is over-engineering.
- **When state must be shared:** **Lifting state up**, as the React docs describe it, is usually enough. Put the state in the closest common parent of the components that read and update it.
- **When prop drilling starts to hurt:** Your first move should be **component composition**. Pass the components that need the state to its owner as `children` or slots, and the components in between no longer carry it. If that isn't enough, reach for **Context**. Context exists to solve prop drilling, not to hold "all of the app's state", and reaching for it too early can cost more than passing a few props. **State that changes often, has many subscribers and is only partly used by each of them is a poor fit for Context.**
- **Server cache is not state:** Treating server data as "global state" leads to architectural mistakes. For invalidation, refetching and optimistic updates, **TanStack Query** is a better step than picking a state library. As of 2026, it's the standard way to handle server data.

![Sharing state through Context](https://omergulcicek.com/blog/2026/02-01/context.png)

In short: **use what React gives you first.** `useState`, `useReducer`, lifting state up, composition and Context carry most apps a long way. A global store belongs in the conversation only when the kind of state, or its subscription and performance needs, pushes past those defaults.

## 3. The breaking point: subscriptions and render performance

Even with local state and Context set up well, the interface can slow down or render more than it should. At that point the problem is usually not the architecture but **subscription management**.

Two symptoms show up again and again:

1. **The data changes but the UI doesn't update:** This usually points to a break in the data flow, or a subscription React can't see change.
2. **The UI re-renders even though the data it shows hasn't changed:** Components are subscribed to too wide a slice of state (the whole Context value, for example), or depend on a value that gets a new reference on every render. The result is wasted re-renders and lost performance.

So next to "where should global state live?", ask **"who subscribes to which data, and is an unchanged value triggering renders because of a new reference?"** If your Context holds one large object, changing any field of it re-renders **every** component that consumes that Context.

The fix is **selector-based subscriptions**, or splitting state into logical pieces with separate providers. Granularity and subscription design matter more than the library you pick. The same goes for debugging: how changes are traced, and how predictable updates stay on a large team, follow directly from your subscription and action model.

## 4. Libraries: the optimization layer

Choosing a library is the last step of this approach: name the kind of state, use React's built-ins, look at your subscription needs, and only then ask "which library?" The table below maps the **kind** of state and your **needs** to a starting point. There is no single "best" option.

For small and mid-sized projects, the combination that pays off most is usually **local state + Context (ambient) + TanStack Query (server cache) + Zustand (global UI)**. It stays true to "use React first" while covering performance and scale. I wrote about keeping a Zustand store's scope narrow in [Zustand and React Context](https://omergulcicek.com/blog/zustand-ve-react-context) (in Turkish).

## Conclusion: a strategy you can apply

State management in React isn't about picking a popular library; it's about understanding what the data is and putting it in the right layer. In modern projects, the approach that pays off most is to handle server data with **TanStack Query** and global UI state with **Zustand**. Combined with React's own tools (useState, Context, composition), that pair gives you an architecture that is both flexible and fast.

For a structure that lasts, follow this order:

1. **Name the kind of state:** UI, server or workflow.
2. **Treat React as the default:** Use useState, lifting state up and composition as far as they go.
3. **Manage the cost of subscriptions:** Make sure only the relevant data triggers a render; move to selectors when needed.
4. **Pick the right tool:**
   - **Data comes from an API:** [TanStack Query](https://tanstack.com/query/latest).
   - **Global settings that rarely change** (theme, language, auth): [Context](https://react.dev/learn/passing-data-deeply-with-context).
   - **Shared UI state that changes often:** [Zustand](https://zustand.docs.pmnd.rs/).
   - **Temporary state owned by one component** (a form, a modal): [useState](https://react.dev/reference/react/useState) / [useReducer](https://react.dev/reference/react/useReducer).

Follow this order and the architecture stays manageable and fast, however large the project grows.

## ViraStack Start

If you'd like to see the "right state, right tool" approach from this post in a real codebase, take a look at the starter I built:

**[ViraStack Start](https://github.com/virastack/start)**: a starter kit for Next.js and TanStack Start projects, with its standards already in place and room to scale.

From state management to folder structure, it's these best practices written down as code. If it helps your projects, a star on GitHub is always appreciated ⭐.

---

## Sources

- [Choosing the Right State Strategy in React](https://jsdev.space/react-state-management/) - JavaScript Development Space
- [Application State Management with React](https://kentcdodds.com/blog/application-state-management-with-react) - Kent C. Dodds
