Documentation
State Management
Decision Tree

State Management Decision Tree

When to use what for managing state in your React application.

Golden Rule: Server data → TanStack Query, Global client state → Zustand, Local UI → useState

Decision Tree

Is state needed by a single component?
├── YES → useState
└── NO → Shared between parent-child only?
    ├── YES → Pass via props
    └── NO → Is it server/async data?
        ├── YES → TanStack Query
        └── NO → Is it form state?
            ├── YES → React Hook Form
            └── NO → Is it global UI state?
                ├── YES → Zustand
                └── NO → React Context

Quick Reference

State TypeSolutionExample
Component UI stateuseStateModal open/close, input value
Complex local stateuseReducerMulti-step form, complex toggles
Form dataReact Hook FormLogin form, settings form
Server dataTanStack QueryUser list, API responses
Global UI (modals, theme)ZustandToast notifications, sidebar state
Auth stateZustand + persistCurrent user, tokens
Feature flagsReact ContextA/B testing, permissions

Detailed Examples

Local Component State

const Counter = () => {
  const [count, setCount] = useState(0);
 
  return (
    <button onClick={() => setCount(c => c + 1)}>
      Count: {count}
    </button>
  );
};

Complex Local State

type State = { step: number; data: FormData };
type Action =
  | { type: 'NEXT_STEP' }
  | { type: 'PREV_STEP' }
  | { type: 'SET_DATA'; payload: Partial<FormData> };
 
const reducer = (state: State, action: Action): State => {
  switch (action.type) {
    case 'NEXT_STEP':
      return { ...state, step: state.step + 1 };
    case 'PREV_STEP':
      return { ...state, step: state.step - 1 };
    case 'SET_DATA':
      return { ...state, data: { ...state.data, ...action.payload } };
  }
};
 
const MultiStepForm = () => {
  const [state, dispatch] = useReducer(reducer, initialState);
  // ...
};

Server State

// Always use TanStack Query for API data
const UserList = () => {
  const { data: users, isLoading, error } = useQuery({
    queryKey: ['users'],
    queryFn: () => userService.getUsers(),
  });
 
  if (isLoading) return <Skeleton />;
  if (error) return <Error />;
 
  return <List items={users} />;
};

Global UI State

// Zustand for global UI state
const useUIStore = create<UIState>((set) => ({
  isSidebarOpen: true,
  toggleSidebar: () => set((s) => ({ isSidebarOpen: !s.isSidebarOpen })),
}));
 
// Use anywhere in app
const Sidebar = () => {
  const isOpen = useUIStore((s) => s.isSidebarOpen);
  return isOpen ? <SidebarContent /> : null;
};

Anti-Patterns to Avoid

Don't store server data in Zustand. Use TanStack Query for automatic caching, refetching, and deduplication.

Don't Store Server Data in Zustand

// ❌ Wrong
const useUserStore = create((set) => ({
  users: [],
  fetchUsers: async () => {
    const users = await api.getUsers();
    set({ users });
  },
}));
 
// ✅ Correct - Use TanStack Query
const useUsers = () => useQuery({
  queryKey: ['users'],
  queryFn: api.getUsers,
});
⚠️
Don't duplicate state. Derive values from source of truth using useMemo.

Don't Duplicate State

// ❌ Wrong - Derived state stored separately
const [items, setItems] = useState([]);
const [filteredItems, setFilteredItems] = useState([]);
 
// ✅ Correct - Derive from source
const [items, setItems] = useState([]);
const [filter, setFilter] = useState('');
const filteredItems = useMemo(
  () => items.filter(i => i.name.includes(filter)),
  [items, filter]
);