Difficulty: Advanced
One of the most important architectural decisions in a React app is choosing where and how to manage each piece of state. There are three main categories: local (component) state, global (client) state, and server (remote) state. Each has different characteristics and optimal tools.
Local state is owned by a single component and managed with useState or useReducer. It is the simplest and most performant option because updates only affect that component and its children. Examples include form input values, toggle states, and modal open/close. The rule of thumb: keep state as close to where it is used as possible (state colocation).
Global client state is data that many components across different parts of the tree need to access - theme, locale, auth status, sidebar open/close. For low-frequency updates with few consumers, React Context works well. For apps that need finer-grained subscriptions, Zustand is an excellent choice: no Provider boilerplate, selector-based subscriptions, and easy access outside React. Redux Toolkit is appropriate for very large teams where strict patterns (actions, reducers, middleware) aid maintainability.
Server state is data owned by a backend: user profiles, product lists, notifications. It can become stale, requires loading/error handling, and benefits from caching and background sync. React Query (TanStack Query) is the standard tool here - it handles caching, deduplication, stale-while-revalidate, and pagination out of the box. Using React Query eliminates the need to store server data in Redux or Zustand.
The decision framework: (1) Is the state used by only one component? Use useState. (2) Is it shared between a parent and a few children? Lift state up. (3) Is it needed globally but changes infrequently? Use Context. (4) Is it needed globally and changes frequently or needs selectors? Use Zustand. (5) Does it come from a server? Use React Query. Mixing these tools is normal and expected - a typical app uses useState for forms, Zustand for auth/theme, and React Query for all API data.
// 1. LOCAL: Toggle is only used here
function Accordion({ title, children }) {
const [isOpen, setIsOpen] = React.useState(false);
return (
<div>
<button onClick={() => setIsOpen(!isOpen)}>{title}</button>
{isOpen && children}
</div>
);
}
// 2. GLOBAL (Zustand): Auth needed everywhere
const useAuth = create((set) => ({
user: null,
login: (user) => set({ user }),
logout: () => set({ user: null }),
}));
// 3. SERVER (React Query): Products from API
function ProductList() {
const { data } = useQuery({
queryKey: ['products'],
queryFn: () => fetch('/api/products').then(r => r.json()),
});
return data?.map(p => <ProductCard key={p.id} product={p} />);
}
Three different state categories, three different tools - each chosen for its strengths. This is the standard modern React pattern.
// BAD: form values in global store
const useFormStore = create((set) => ({
name: '',
email: '',
address: '',
setName: (name) => set({ name }),
setEmail: (email) => set({ email }),
setAddress: (address) => set({ address }),
}));
// Every keystroke updates the global store
// All subscribers re-render on every keystroke
// The form is only on one page!
// GOOD: use local state for forms
function ContactForm() {
const [form, setForm] = React.useState({ name: '', email: '', address: '' });
// Only this component re-renders on keystrokes
}
Not all state needs to be global. Form data used by one component should stay local. Global stores are for data that multiple distant components need.
// Zustand for client state
const useUIStore = create((set) => ({
sidebarOpen: false,
toggleSidebar: () => set((s) => ({ sidebarOpen: !s.sidebarOpen })),
}));
// React Query for server state
function Dashboard() {
const sidebarOpen = useUIStore((s) => s.sidebarOpen);
const toggleSidebar = useUIStore((s) => s.toggleSidebar);
const { data: stats } = useQuery({
queryKey: ['dashboard-stats'],
queryFn: fetchDashboardStats,
staleTime: 30_000,
});
return (
<div className={sidebarOpen ? 'with-sidebar' : ''}>
<button onClick={toggleSidebar}>Toggle Sidebar</button>
<StatsGrid stats={stats} />
</div>
);
}
Zustand handles UI state (sidebar). React Query handles server data (stats). Each tool does what it is best at. No overlap, no confusion.
local state, global state, server state, state colocation, decision framework