How to Manage State in React Applications
SkillVeris Team
Engineering Team

Managing state in React means picking the right scope for each piece of data: local component state, shared state across a subtree, global app state, or server state from an API.
In this guide, you'll learn:
- Start with useState and useReducer for local state, and only reach for bigger tools when data genuinely needs to be shared widely.
- React Context shares values across a component tree without prop drilling, but it is not built for high-frequency updates.
- Server state — data fetched from an API — is best handled by libraries like TanStack Query that manage caching, refetching, and loading states.
- Global client stores like Redux Toolkit, Zustand, and Jotai centralise complex app-wide state with predictable updates.
1How to Manage State in React
Managing state in React comes down to choosing the right scope and tool for each kind of data. State falls into a few categories — local UI state, state shared across part of the tree, global application state, and server state from an API — and each is best handled differently. There is no single correct tool; the skill is matching the mechanism to the need.
The guiding principle is to keep state as local as possible and only lift or centralise it when data truly needs to be shared. Reaching for a heavy global store on day one is the most common way React apps become harder to maintain than they need to be.
2The Four Kinds of State
Before choosing a tool, identify what kind of state you are dealing with. Mixing these categories is the root of most state-management confusion, because each has different needs around sharing, caching, and updates.
- Local state: belongs to one component — a form input, a toggle, a hover flag.
- Shared state: needed by several components in a subtree — a theme, the current user.
- Global state: touches much of the app — auth status, a shopping cart, notifications.
- Server state: data fetched from an API that must stay in sync with a backend.
- URL state: values that live in the address bar, like filters, tabs, and pagination.
🔑Categorise First
Ask what kind of state you have before choosing a library. Server state and client state have completely different needs — treating an API response like local state is a frequent source of bugs.
3Local State: useState and useReducer
Most state should stay local. useState handles simple values, while useReducer suits state with multiple sub-values or complex transitions, centralising update logic in a single reducer function. Both keep data inside the component that owns it, which is the easiest code to reason about and test.
- const [open, setOpen] = useState(false); // simple local toggle
- const [state, dispatch] = useReducer(reducer, initialState); // complex logic
- dispatch({ type: 'increment' }); // updates via described actions
- // useReducer shines when the next state depends on the action and prior state
Lifting State Up
When two sibling components need the same state, move it to their nearest common parent and pass it down as props. This built-in pattern, called lifting state up, solves a surprising amount of sharing without any extra library.
4Sharing State With Context
React Context lets you share a value across a whole subtree without passing props through every intermediate component — a problem known as prop drilling. It is ideal for low-frequency, broadly needed values like the current theme, the logged-in user, or a language setting.
- const ThemeContext = createContext('light');
- <ThemeContext.Provider value={theme}>...</ThemeContext.Provider>
- const theme = useContext(ThemeContext); // read anywhere below
- // great for theme, auth, locale — not for fast-changing data
⚠️Context Is Not a State Manager
Every consumer re-renders when a Context value changes. For data that updates frequently, this causes performance problems. Use Context for stable, widely shared values, not as a substitute for a proper store.
5Handling Server State
Data fetched from an API is server state, and it has needs local state does not: caching, background refetching, loading and error tracking, and staying fresh. Dedicated libraries handle all of this so you are not manually juggling useEffect and useState for every request. This is one of the biggest quality-of-life upgrades in modern React.
- TanStack Query (React Query): caching, refetching, and request deduplication.
- SWR: a lightweight alternative built around stale-while-revalidate.
- RTK Query: data fetching bundled with Redux Toolkit.
- Benefits: automatic loading/error states, cache invalidation, and less boilerplate.
Why Not Just useEffect
You can fetch with useEffect, but you then reimplement caching, deduplication, retries, and refetch-on-focus yourself, usually incompletely. A server-state library gives you all of that, tested, so you avoid subtle staleness and race-condition bugs.
6Global Client State Libraries
When genuinely app-wide client state grows complex, a dedicated store keeps updates predictable and debuggable. Several mature options span a range of philosophies, from structured and explicit to minimal and flexible.
- Redux Toolkit: the modern, less-verbose Redux — predictable, great devtools, structured.
- Zustand: a tiny, hook-based store with minimal boilerplate and no providers required.
- Jotai: atomic state where you compose small pieces of state that update independently.
- Recoil / MobX: other approaches to shared reactive state with different trade-offs.
When You Actually Need One
Reach for a global store when many unrelated parts of the app read and write the same complex state, or when you need time-travel debugging and strict update discipline. For most small and medium apps, useState, Context, and a server-state library cover everything.
7Choosing the Right Approach
A simple decision path prevents both under- and over-engineering. Move outward only when the current level cannot serve the data's scope.
- One component needs it: useState or useReducer.
- A few nearby components need it: lift state up to a shared parent.
- Many components across the tree need a stable value: React Context.
- It comes from an API: a server-state library like TanStack Query.
- Complex, app-wide client state: Redux Toolkit, Zustand, or Jotai.
💡Start Small
Begin with local state and add complexity only when a real sharing or performance need appears. It is far easier to introduce a store later than to unwind one you did not need.
8Common Mistakes to Avoid
State-management pain in React usually comes from choosing the wrong tool for the scope, not from any tool being bad.
- Reaching for Redux on day one when useState would do — needless boilerplate.
- Storing server data in a global client store and hand-writing caching logic.
- Putting fast-changing values in Context, causing widespread re-renders.
- Duplicating the same state in multiple places instead of a single source of truth.
- Lifting state higher than necessary, coupling unrelated components together.
9Key Takeaways
Effective React state management follows a few durable principles.
- Identify the kind of state — local, shared, global, or server — before picking a tool.
- Keep state as local as possible; lift or centralise only when sharing demands it.
- Use Context for stable, widely shared values, not high-frequency updates.
- Handle API data with a server-state library like TanStack Query, not raw useEffect.
- Add a global store only when app-wide complex state genuinely requires one.
10Frequently Asked Questions
Q: Do I need Redux for every React app? A: No. Most apps do fine with useState, useReducer, Context, and a server-state library. Redux Toolkit or another global store earns its place only when many unrelated parts of the app share complex client state or you need strict, debuggable update flows. Start simpler and add it if a real need appears.
Q: What is the difference between client state and server state? A: Client state is owned entirely by your app — UI toggles, form inputs, selected tabs. Server state originates from an API and must stay in sync with a backend, needing caching, refetching, and staleness handling. Because their needs differ, server state is best managed by a dedicated library rather than a general store.
Q: When should I use Context instead of a state library? A: Use Context to share stable, infrequently changing values like theme, locale, or the current user across many components without prop drilling. Because every consumer re-renders when the value changes, avoid Context for fast-updating data and use a store like Zustand or Redux Toolkit instead.
Q: Is useState enough for a large application? A: Often for far more than people expect. Combined with lifting state up, Context for shared values, and a server-state library for API data, useState handles the majority of cases. A global store becomes worthwhile only when complex client state is shared widely, so reach for one when that need is concrete rather than anticipated.
Related Reading
Get The Print Version
Download a PDF of this article for offline reading.
About the Publisher
SkillVeris Team
Engineering Team
Our engineering writers turn abstract code concepts into hands-on, project-driven learning experiences.
View all postsRelated Posts
Never miss an update
Get the latest tutorials and guides delivered to your inbox.
No spam. Unsubscribe anytime.