State is the data that changes while the app runs. Most bugs in AI-generated apps are state bugs: stale data, duplicated sources of truth and unclear ownership. The fix is a simple rule for where each kind of state lives.
Core concepts
- Local component state: Use
useStateoruseReducerfor UI details only one component cares about, such as an open dropdown or a text input. — React docs - Context for app-wide stable values: Theme, current user and settings change rarely, so Context is fine. Avoid putting fast-changing data in Context because every consumer re-renders. — React docs
- Client state with Zustand: Zustand is a small store for state that belongs to the client, such as the cart, filters or onboarding progress. It can persist to storage with middleware. — Zustand
- Server state with TanStack Query: Data that lives on a server (lists, profiles) should be fetched and cached with TanStack Query, which gives caching, retries, background refresh and invalidation. — TanStack Query
- One source of truth: Do not copy server data into a client store. Let the query cache own it and derive what you need. — TanStack Query
- Mobile-specific wiring: Connect React Query's online and focus managers to NetInfo and AppState so refetching follows the real network and app foreground state. — TanStack Query React Native guide
Concept videos
YouTube channels and searches for these topics. On a large screen they open in the reading panel next to the roadmap.
- State management for React Native — YouTube search: React Native Zustand TanStack Query
- Mobile app architecture — notJust.dev channel
Explanation
The decision ladder
Start with local state. Lift it up only when two components need it. Use Context for stable global values. Use Zustand for shared client state. Use TanStack Query for anything that comes from a server. Most apps need only the last two.
Why not put everything in a global store
A giant store makes every screen depend on everything, and agents add fields without removing old ones. Narrow stores with one purpose stay readable.
Optimistic updates
For snappy interactions, update the cache immediately and roll back on error. Ask the agent to implement rollback explicitly; it often forgets.
What Is It?
State management is how an app stores, shares and updates data over time.
Separating client state from server state keeps each simple.
Why Should I Care?
Clear ownership prevents stale screens and the 'it works after I restart the app' class of bugs.
Real World Analogy
Local state is a sticky note on your desk, Context is the office whiteboard, Zustand is the team's shared spreadsheet, and TanStack Query is the filing cabinet in another building with a courier that keeps copies fresh.
Visual Mental Model
UI-only -> useState
client-wide -> Zustand
from server -> TanStack Query cache
stable app values -> ContextBeginner Explanation
Keep form text and toggles in useState, keep filters and preferences in a small Zustand store, and fetch lists and profiles with TanStack Query.
Smallest Possible Example
import { create } from 'zustand';
type CartState = { ids: string[]; add: (id: string) => void; clear: () => void };
export const useCart = create<CartState>((set) => ({
ids: [],
add: (id) => set((s) => ({ ids: [...s.ids, id] })),
clear: () => set({ ids: [] }),
}));Common Mistakes
❌ Copying fetched data into a Zustand store.
Now you have two sources of truth to keep in sync. Let the query cache own server data.
❌ Putting a changing list in Context.
Every consumer re-renders on each change; use a store with selectors.
❌ Never invalidating queries after a mutation.
Screens show stale data until restart.
Real Project Example
TanStack Query and Zustand are widely used together in React and React Native apps; the combination appears in many production codebases and starter kits.
Build This Mini Project
Build a tasks app where tasks come from an API via TanStack Query and filters live in a persisted Zustand store.
Interview Question
What is the difference between client state and server state?
Client state is owned by the app (UI choices, drafts); server state is owned by a backend and cached on the client, so it needs fetching, caching, invalidation and sync handling.
Key Takeaways
- Climb the ladder: local, context, Zustand, query.
- Server state belongs in TanStack Query.
- One source of truth per piece of data.
- Invalidate or update caches after mutations.
Stack for this step
| Layer | Pick | Why |
|---|---|---|
| Client state | Zustand | Tiny, selector-based, persistable. |
| Server state | TanStack Query | Caching, retries, refetching. |
| Stable values | React Context | Theme, session, settings. |
Prompts for Cursor, Claude Code & Codex
Paste these into Cursor, Claude Code, Codex, or any coding agent. Replace the bracketed parts with your app details, and keep the agent in plan mode first so you review the approach before it edits files.
Decide where state lives
List every piece of state in SPEC.md. For each, decide: local, context, Zustand, or TanStack Query, with one sentence why. Output a table. Flag any data that would have two sources of truth.Build the store
Create lib/stores/filters.ts with Zustand: filters, setFilter, reset, persisted with AsyncStorage via createJSONStorage. Add selectors in the component so only the needed slice re-renders.Queries and mutations
Create hooks/useItems.ts with TanStack Query: list, create with optimistic update and rollback, invalidate on success. Wire onlineManager to NetInfo and focusManager to AppState.Claude Code skill for this step
Save this as .claude/skills/state-ownership/SKILL.md in your project so Claude Code loads it automatically when the task matches. The same text works as a Cursor rule or an AGENTS.md section.
---
name: state-ownership
description: Use when adding data or state to the app, to decide where it lives and avoid duplicate sources of truth.
---
1. Ask: is it UI-only, client-wide, or from a server?
2. Server data goes through a TanStack Query hook in hooks/; never copy it to a store.
3. Shared client data goes in a small Zustand store with selectors.
4. After mutations, update or invalidate the related queries.Rules, databases & blogs to use
- Zustand rule: store conventions
- TanStack Query v5 rule: query conventions
- React Query rule: alternate query rule
- Redux TypeScript rule: if you prefer Redux
- AI chat app with Firebase: real state and data flow
Resources
The 60-second story
Think of an office. A sticky note on your own desk is local state: only you read it. The whiteboard by the door is context: everyone sees it, but you rarely change it. The team spreadsheet is Zustand: shared, edited often, owned by the team. And the archive in another building is server state, with a courier (TanStack Query) who fetches copies and refreshes them when the originals change.
Trouble starts when someone photocopies the archive into the team spreadsheet. Now two versions exist and they drift apart. The rule is simple: one owner per fact.
Mental model: one owner per piece of data, and the courier handles the archive.

