Introduction
State management discussions often start with the wrong question: Which library should we use?
The better question is: Where should business logic live?
A state manager is not just a place to store values. It shapes how the application represents user intent, server data, derived state, errors, loading states, optimistic updates, permissions, and cross-feature communication.
Choosing a library before understanding those responsibilities is how teams end up with beautiful stores that still contain messy architecture.
First: What Kind of State Is It?
Not all state is the same.
A frontend application usually contains several categories:
- Server state: data owned by the backend.
- UI state: local visual state like open tabs and panels.
- Form state: temporary, often invalid user input.
- Domain state: client-owned business concepts.
- Session state: current user, auth, roles, permissions.
- Navigation state: URL params, route state, browser history.
- Derived state: values calculated from other state.
Putting all of these into one global store is convenient at first and expensive later.
Each type has different ownership and lifecycle.
Server State Is Not Client State
Server state should usually be handled by a query/cache layer, not a hand-rolled global store.
Server state needs:
- Fetching.
- Caching.
- Invalidation.
- Refetching.
- Stale data policies.
- Error and retry behavior.
- Synchronization after mutation.
Tools like TanStack Query exist because this problem is bigger than useState.
You can store API responses in Zustand or MobX, but then you must rebuild cache policy yourself. Sometimes that is justified. Often it is not.
Lazy architecture uses the tool that already matches the lifecycle.
UI State Should Stay Local Until It Cannot
Local UI state is cheap and readable.
If a dropdown is open, keep that state near the dropdown. If a modal belongs to one feature, keep it near the feature. If a form field is only relevant before submit, keep it inside the form.
Promoting local state to global state should require a reason:
- Multiple distant components need it.
- It must survive navigation.
- It must coordinate with another domain concept.
- It must be inspected or controlled by infrastructure.
Otherwise, global state becomes a junk drawer.
Business Logic Needs a Home
State managers become dangerous when they are treated as storage only.
Consider this mutation:
set({ selectedProjectId: id });
That may be fine. But what if selecting a project also clears a task draft, closes a modal, updates permissions, and syncs the URL?
Now selection is not just a value update. It is a business action.
Name it:
selectProject(id: ProjectId) {
// coordinate the actual product behavior here
}
A good state model exposes user intent, not random setters.
Derived State Should Be Derived
Duplicated state is one of the easiest ways to create bugs.
If visibleTasks can be calculated from tasks, filters, and search, it should usually be derived rather than stored separately.
Derived state has two benefits:
- It reduces synchronization bugs.
- It documents the relationship between values.
The exception is performance. If derivation is expensive, cache it with selectors, memoization, computed values, or the state manager's native mechanism.
But do not duplicate first and justify later.
Events, Actions, Atoms, Stores, Observables
Different libraries make different concepts cheap:
- Zustand makes small stores and selectors easy.
- Effector makes events, effects, and explicit data flow central.
- Reatom encourages atomized state and derived relations.
- MobX makes observable models and computed values ergonomic.
- Signals make fine-grained dependencies direct.
The library's philosophy matters because it changes how the team thinks.
If the app has many independent reactive pieces, atomized systems may feel natural. If the app is model-heavy and benefits from object-like ViewModels, MobX may fit. If the app is small, React state plus a query library may be enough.
The mature answer is not "my favorite tool." It is "this tool matches the shape of our state."
Store vs Service
A recurring frontend architecture question: should business operations live in the store or in a service?
The answer depends on what the operation is.
A store is good at holding state, exposing derived values, and coordinating state transitions.
A service is good at talking to infrastructure, implementing use cases, and hiding low-level details.
If a store directly owns HTTP, transformation, permissions, optimistic updates, analytics, and UI state, it may become too powerful. If every state update goes through services, simple UI interactions become bureaucratic.
Use the smaller boundary that keeps the code understandable.
Cross-Domain Communication
State management becomes hardest when domains need to talk.
For example, archiving a project may affect:
- Project list.
- Task board.
- Notifications.
- Permissions.
- URL state.
Direct imports between every domain create coupling. A facade or application service can coordinate the use case without making domains know too much about each other.
This is where architecture matters more than library choice. The wrong dependency direction will hurt in any state manager.
Conclusion
State management is business logic placement.
Before choosing a library, identify the kinds of state in the application, who owns them, how long they live, and which user actions change them.
Then pick the smallest tool that matches the problem. Local state for local UI. Query cache for server data. Stores or models for client-owned domain state. Services or facades for cross-domain coordination.
The best state management decision is not the most fashionable one. It is the one that makes product behavior explicit and future change boring.