"State management" in React covers several different problems. Teams run into trouble when they put all of them in one global store. Sort your state by type first, and the tool choice becomes straightforward.
The five kinds of state
- Local UI state: an open dropdown, a form input value, a selected tab.
- Shared client state: a shopping cart, theme, sidebar visibility, or a multi-step wizard.
- Server state: data that lives on a server and that you cache on the client, such as products or user profiles.
- URL state: search filters, page number, sort order and selected item.
- Form state: values, validation and submission status.
Start with local state
Use useState or useReducer inside the component that owns the data. If two siblings need the same value, lift it to their closest common parent. This handles most cases and keeps components easy to test.
function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount((c) => c + 1)}>{count}</button>;
}
Use useReducer when the next state depends on several fields or when many actions change the same object. A reducer centralizes the rules and makes transitions explicit.
Treat server data as a cache
Do not copy API responses into Redux or Zustand and write loading flags by hand. TanStack Query or SWR manage fetching, caching, refetching, retries and invalidation. Components read from the cache with a hook, and mutations invalidate the affected keys. Removing hand-written server state logic typically deletes a large share of an app's state code.
Put shareable state in the URL
If a user would want to bookmark it, share it or use the back button on it, it belongs in the URL. Store filters and pagination in query parameters. In Next.js, read them with useSearchParams and update with the router. The page then restores itself after a refresh and works from a copied link.
Use a small store for shared client state
When unrelated components need the same client-only data, a lightweight store avoids prop drilling. Zustand has a compact API and no provider:
import { create } from "zustand";
type CartState = {
items: { id: string; qty: number }[];
add: (id: string) => void;
clear: () => void;
};
export const useCart = create<CartState>((set) => ({
items: [],
add: (id) => set((s) => ({ items: [...s.items, { id, qty: 1 }] })),
clear: () => set({ items: [] }),
}));
// In a component: select only what you need
const count = useCart((s) => s.items.length);
Selecting a slice, as in the last line, re-renders the component only when that slice changes. Zustand also provides a persist middleware for saving state to local storage.
Context for stable values
React Context works well for values that change rarely: the theme, the current locale, an authenticated user object. Every consumer re-renders when the context value changes, so avoid putting fast-changing state in it, or split it into several smaller contexts.
When Redux Toolkit still fits
Redux Toolkit suits large applications with complex client-side workflows, many developers who benefit from strict conventions, and a need for time-travel debugging and middleware. It removed most old boilerplate. For a typical marketing site or SaaS dashboard, it is more machinery than you need.
Form state
Use React Hook Form or a similar library for forms with validation, and pair it with a schema from Zod. Keep field values out of global stores. Uncontrolled inputs avoid re-rendering the whole form on each keystroke.
Decision guide
- Only one component uses it:
useState. - Siblings use it: lift it up.
- It came from an API: TanStack Query.
- Someone might share the link: URL parameters.
- Distant components need it and it is client-only: Zustand or Context.
- It is a form: React Hook Form.
Keep state minimal
Store the smallest source of truth and derive the rest during render. If you can compute total from items, do not keep both in state. Duplicated state drifts out of sync and causes bugs that are hard to trace. Normalize large collections by ID, and keep stores free of components and functions that cannot serialize.
Common questions
Is Redux dead?
No, but it is no longer the default. Redux Toolkit remains a solid choice for large apps with complex client logic. Many apps now get by with TanStack Query for server data, Zustand or Context for a small amount of shared state, and the URL for filters.
Why does my component re-render so often?
Common causes are new object or function props created on every render, context values that change, and selecting a whole store instead of a slice. Use the React DevTools Profiler to find the cause before adding memo or useMemo. The React Compiler can remove much of that manual work in new projects.
How do I persist state between visits?
Persist only what users expect to survive, such as a cart or theme. Zustand's persist middleware writes to local storage. Store drafts of long forms too, and clear sensitive data on sign-out.
If your React app has tangled state and slow renders, our team can untangle it. Send us your repo or see our React development services.

