Modern React applications need to manage state, but state itself isn’t a single concept—it’s split across local values, shared values, and external data from APIs. This divergence creates confusion, especially when trying to decide between tools like useContext
and React Query. While they appear to serve similar goals—sharing and consuming state across components—their core purposes differ fundamentally. Many developers attempt to use useContext
for things better suited to React Query and vice versa, leading to mismanaged state, unnecessary re-renders, and complex dependency chains. The real challenge lies in understanding when to use each—and more importantly, when not to.
Core Responsibilities
Understanding the strengths of each tool begins with clarity about what kind of data is being handled. useContext
is best suited for sharing React state across components. It works well for things like:
- Theme toggling
- Authentication state
- User preferences
- Global app settings
These are values that originate in the UI and often change based on user actions. They’re typically managed through hooks like useReducer
or useState
, then passed down using Context.
React Query, by contrast, is purpose-built for managing remote data. Anything coming from an API—be it products, users, or messages—fits React Query’s model. It handles caching, refetching, background synchronization, and error states without forcing you to manually write boilerplate logic. That distinction is crucial: useContext
is great for shared state, but React Query shines with server state.
Performance Trade-offs
Using useContext
for API data might seem logical at first. A common pattern involves fetching data in a parent component, storing it in state, and then sharing it via a context provider. But this approach often leads to performance bottlenecks. When that shared context updates—even if only one value inside it changes—every consuming component re-renders.
React Query solves this problem with fine-grained subscriptions. Each component only re-renders when the specific query it subscribes to changes. If five components depend on different pieces of server data, React Query will ensure that each only updates when its own data changes, not when the rest of the app does. This subtle efficiency becomes especially important in larger apps with more moving parts.
Consider this comparison:
Feature | useContext | React Query |
---|---|---|
Best for | Shared app-level state | Remote server data |
Built-in caching | ❌ Manual | ✅ Automatic |
Background refetching | ❌ Not supported | ✅ Fully supported |
Re-render granularity | ❌ Global updates | ✅ Query-level updates |
Error and loading states | ❌ Manually handled | ✅ First-class support |
Devtools support | ❌ None | ✅ With React Query Devtools |
Mutation support | ❌ Custom logic needed | ✅ Built-in mutation management |
Developer Experience
There’s a certain appeal in using useContext
everywhere—it’s built into React, predictable, and doesn’t require installing anything. But with simplicity comes extra responsibility. Developers must manually manage loading states, write conditional renders, cache results if needed, and avoid unwanted re-renders. Over time, these responsibilities pile up and lead to inconsistent patterns across the codebase.
React Query abstracts much of this boilerplate. Fetch status, error handling, and stale-while-revalidate logic are all encapsulated in its APIs. This consistency leads to better developer experience and more predictable behavior. Moreover, features like retrying failed requests or pagination support are available out of the box, which could otherwise require custom context logic and reducers.
Combined Usage
Rather than viewing these tools as competitors, they’re often most powerful when used together. For instance, React Query can handle all data fetching—whether it’s user profiles, product listings, or dashboard metrics—while useContext
can manage things like dark mode, current language, or user roles.
This layered architecture helps decouple server logic from UI logic. A context provider doesn’t need to know anything about fetching data from the backend. Instead, it can focus purely on UI behavior and preferences. Similarly, components relying on external data don’t need to manage app-level state—they can focus solely on rendering based on fresh, cached data from the server.
Here’s an example of a smart division of responsibilities:
React Query handles:
- Fetching and caching user data
- Polling for notifications
- Stale query invalidation on tab refocus
- Offline retry behavior
useContext handles:
- Theme (light/dark)
- User login session (token, role)
- Language preferences (i18n)
- Modal state or sidebar toggling
Reusability and Testing
Context-based logic often becomes tangled with UI rendering. If a context provides a large object with deeply nested state, it’s harder to test and harder to refactor. Smaller, focused contexts improve this, but React Query naturally encourages separation between data and presentation.
Hooks like useQuery
or useMutation
return a consistent object structure, which makes them easy to mock in unit tests or reuse across components. For instance, if a product list is needed in three different parts of the app, the same query hook can be reused without lifting state to a parent or creating a new context. This aligns closely with modern React testing practices like those explored in React Testing Library, where separation of concerns makes tests simpler and more reliable.
Real-World Friction
One mistake seen often is developers using useContext
to wrap large chunks of API-driven state simply because the API call happens once at the top level. That call is usually followed by a global context update that causes massive re-renders when the data changes. It feels clean during early development but quickly becomes brittle and hard to scale.
Another issue arises when teams try to implement caching or background refetch logic inside a custom context. They end up re-inventing what React Query already does well—without reaching the same level of reliability. Over time, these custom-built patterns accumulate bugs, slow performance, and grow increasingly difficult to maintain.
Even with a minimal setup, using tools that are designed for the job—like QueryClient—provides stability and long-term sustainability without having to invent your own patterns.
Conclusion
Choosing between useContext
and React Query isn’t about picking the superior tool. It’s about understanding their boundaries. Context is ideal for local UI-driven state that needs to be shared globally. React Query is designed for fetching, caching, and synchronizing remote data.
Using them together—not interchangeably—leads to cleaner, faster, and more scalable React applications. The real clarity comes from not just knowing how to use these tools, but when to stop using one and start using the other.