This article is published in English.
React Query and Redux: Rethinking Server State in Large Apps
Learn why a production chat app used TanStack Query instead of Redux to manage server data, and where Redux still fits in modern React architecture.
Picture a developer moving from smaller projects into a product-based company, eager to finally see how large-scale, production-grade software is actually built. After years of working on frontend applications, joining a team responsible for software used by millions of people changes the way you evaluate code.
You stop asking simply, "Does this feature work?"
Instead you start asking:
How does this architecture hold up under millions of users? How is state kept in sync across the whole application? What happens when ten different components all need the same piece of data? How do engineers keep such a large codebase maintainable as the product keeps growing?
Now imagine that developer is assigned to one of the core modules of the product — a chat application, similar in spirit to Slack.
This isn't a minor feature tucked away in a corner of the app.
The chat experience sits at the heart of the product, serving millions of users and tightly connected to critical business workflows, revenue-driving experiences, and some of the company's most important enterprise customers.
So it's reasonable that, when digging into the codebase for the first time, this developer would have a fairly predictable expectation going in.
A chat application at this scale inevitably deals with a huge volume of information that many screens need to share: threads and their messages, how many messages remain unread, who's participating in a conversation, paging through history, edits and other write operations, in-flight loading indicators, and live updates arriving in real time.
Given all that, you'd expect to find the usual suspects in a large React codebase:
Redux, Zustand, or at least some form of global state management.
But after searching through the code, there's no store to be found.
No Redux setup.
No Zustand.
No sprawling Context object holding the app's shared data.
At first glance, it would be easy to assume something was simply missed while exploring the repo.
Digging further, though, reveals what is actually driving the shared data layer.
TanStack Query.
That's a genuinely surprising discovery if your mental model of the library is limited.
Many developers think of React Query mainly as a tool for fetching API data, caching responses, tracking loading states, and refetching when something changes.
Yet in this production-grade application serving millions of users, the Query cache was doing far more than that. It functioned as the shared home for all server-owned data across the entire app.
That observation challenges a long-held assumption about frontend architecture.
Maybe the right question isn't:
"Why isn't there a Redux store here?"
Maybe it should be:
"Why would server-owned data need to live in Redux in the first place?"
That question opens up a much deeper conversation about how we model state in React apps, and why, in many real-world systems, TanStack Query can quietly remove a surprising share of the global-state machinery teams have historically built by hand.
From Redux to React Query
There was a period when wiring an API call into a React app felt like a lightweight task.
Then Redux entered the picture.
The API call itself stayed simple. Everything wrapped around it got complicated.
You'd define an action.
Write a reducer.
Add loading and error flags.
Dispatch the action.
Save the response into the store.
Write a selector to read it back out.
Wire the component up to all of it.
Then, weeks or months later, someone inevitably asks:
"Why does this data look stale?"
And the fix is usually another action just to force a refetch.
Having gone through that cycle repeatedly, an uncomfortable pattern becomes clear:
Global state tools were frequently being used to manage something that was never really a client-state concern to begin with.
The backend was the true owner of the data.
The frontend was only ever consuming it.
That distinction is a big part of why TanStack Query has become such an interesting alternative in modern React applications.
First, What Exactly Is React Query?
Before exploring how it cuts down on the need for Redux, it's worth clearing up a common misunderstanding.
TanStack Query, previously called React Query, is not a substitute for useState, Redux, or Zustand.
Its core responsibility is managing server state — data that originates outside your React app and needs to be fetched, cached, kept in sync, updated, and eventually treated as stale.
React Query isn't just about firing off a request and dropping the response into a component's local state.
TanStack's own documentation frames the library specifically around fetching, caching, synchronizing, and updating server state.
Think of data like:
Users
Projects
Messages
Notifications
Orders
Analytics
Your frontend doesn't actually own any of this information.
The backend does.
React Query acts as the layer between your UI and that backend, taking responsibility for the lifecycle of that data.
At the core of this design sits the Query Cache.
According to TanStack's current documentation, QueryCache is the storage layer for queries — holding their data, metadata, and status. A QueryClient owns this cache and exposes the APIs an application uses to read from it, update it, invalidate entries, and otherwise interact with it.
That's precisely why several unrelated parts of an app can request the same query and get consistent results:
const { data } = useQuery({
queryKey: ['projects'],
queryFn: fetchProjects
})
React Query does much more than cache a single response, though.
It manages caching, deduplication of requests, freshness tracking, background refetching, retry logic, garbage collection, mutations, and invalidation, all as part of the same system.
For instance, after a project gets updated:
const queryClient = useQueryClient()
await updateProject(project)
queryClient.invalidateQueries({
queryKey: ['projects']
})
Instead of manually instructing ten separate components how to update their local copy of the project, you simply tell the query system:
"The server data behind this query may no longer be accurate."
From there, the cache takes care of revalidating it.
That's the essential idea underpinning TanStack Query.
It isn't attempting to become a second Redux.
It's giving server state a lifecycle of its own.
Once you start treating server state as fundamentally different from client state, it becomes much easier to see why an application that looks, on the surface, like it needs a sprawling Redux store might not need one at all.
Redux Was Never the Problem
To be fair, Redux itself deserves no blame here.
Redux Toolkit continues to be the officially endorsed approach for writing Redux, and Redux still earns its keep whenever an application genuinely needs intricate client-side state, predictable transitions between states, middleware pipelines, or one unified state model.
Trouble begins when developers dump absolutely everything into a single global store.
Picture an application shaped like this:
{
user: {},
projects: [],
teams: [],
notifications: [],
orders: [],
products: [],
analytics: {},
theme: "dark",
sidebarOpen: true
}
At a glance, all of that looks like it belongs to the application.
It doesn't, though.
Try asking one straightforward question:
Who actually owns this data?
Is the list of projects something the React app controls?
Not really.
The backend is the true owner.
Could a different user modify an order while your browser tab stays open?
Sure could.
Could a notification show up without any action from your React code?
Yes, absolutely.
Could the server unilaterally revoke or change a user's permissions?
Without question.
So a substantial chunk of that "application state" isn't owned by the frontend at all.
It's server state.
And server state comes with an entirely different category of challenges.
You have to fetch it.
You have to cache it.
You have to figure out when it's gone stale.
You have to refetch it.
You have to keep it synchronized after mutations happen.
You have to manage loading indicators and error states.
You have to account for retries and dropped network connections.
This is the exact set of problems that TanStack Query was built to handle.
The Architectural Shift
A conventional, Redux-centric setup typically resembles this flow:
API
↓
Async action / thunk
↓
Reducer
↓
Redux Store
↓
Selector
↓
React Component
For a lot of API-driven apps, that flow can be reshaped into something like:
API
↓
TanStack Query
↓
Query Cache
↓
React Component
The difference might seem minor on paper.
It isn't.
The real shift is that you no longer hand-build all the plumbing needed for server state.
Take something as ordinary as a list of projects.
Under Redux, you'd typically start with:
const initialState = {
data: [],
loading: false,
error: null
}
Then add an async action:
dispatch(fetchProjects())
Followed by reducer logic covering:
pending
fulfilled
rejected
And finally, a selector:
const projects = useSelector(
state => state.projects.data
)
Now set that next to the TanStack Query version:
const { data, isPending, error } = useQuery({
queryKey: ['projects'],
queryFn: fetchProjects
})
That's not merely a reduction in lines of code.
The query itself becomes the abstraction that wraps the server resource.
The query key names the resource being tracked.
The cache holds onto the result.
The query keeps tabs on its own status.
And any number of components can pull from that same cached value.
TanStack Query's QueryCache exists specifically to hold query results along with their associated state, and QueryClient gives you the interface for working with that cache.
The Query Cache Is Basically a Global Store for Server Data
This might be the single most important concept here.
Plenty of developers hear the phrase:
"React Query has a cache."
And assume it means:
"So it's just caching API responses."
But it goes further than that.
The query cache effectively becomes the shared, single source of truth for your app's server state.
Picture three separate components:
Dashboard
|
+── ProjectList
|
+── ProjectSidebar
|
+── RecentProjects
Every one of them needs the same data, keyed by:
['projects']
There's no need to manually funnel that server data into Redux first and then have all three components read from the store.
You just request the identical query wherever it's needed:
useQuery({
queryKey: ['projects'],
queryFn: fetchProjects
})
TanStack Query takes care of sharing and caching that data behind the scenes.
The resulting architecture can end up looking like:
React App
|
┌─────────┴─────────┐
| |
Client State Server State
| |
Redux / Zustand TanStack Query
| |
UI state Query Cache
Suddenly the whole system becomes much simpler to reason about.
So Can React Query Replace Redux?
In some applications, yes, it genuinely can.
But here's the crucial nuance:
It doesn't replace Redux by being a superior version of Redux.
Instead, it removes the need to lean on Redux for server state in the first place.
That's a meaningfully different claim.
TanStack Query's own documentation explicitly frames server-state management as a distinct problem from client-state management, and it points out that once server state moves over to React Query, whatever client state remains to be managed globally can shrink dramatically.
That's where things start to get genuinely interesting from an architecture standpoint.
You might land on a split like this:
Client State
theme
sidebar
selectedTab
modal
editor
filters
Alongside:
Server State
users
projects
orders
notifications
products
analytics
At that point your tooling choices become far more targeted:
Client State → Redux / Zustand / Context / React
Server State → TanStack Query
Rather than expecting one single store to shoulder both jobs at once.
But Redux Still Has a Job
Consider a different kind of application, something closer to a design tool like Figma.
You might end up storing state such as:
{
selectedLayer,
activeTool,
zoom,
canvasMode,
dragState,
undoStack,
redoStack
}
None of that is server state.
The frontend owns it outright.
It changes synchronously, in direct response to user interaction.
Multiple parts of the UI depend on it simultaneously.
And you might need to coordinate intricate transitions as one piece of state affects another.
This is exactly the kind of scenario where a dedicated client-state manager earns its keep.
TanStack Query's documentation makes a similar point: complicated, UI-driven state that has nothing to do with a server can still justify bringing in a purpose-built client-state tool.
So the takeaway isn't "rip Redux out everywhere and replace it with React Query." That would be an overcorrection.
And There's One More Important Player: RTK Query
There's an additional wrinkle worth mentioning.
Redux Toolkit already ships with RTK Query, a data-fetching and caching layer designed specifically to work inside Redux applications.
It can generate hooks automatically and take care of endpoint fetching, loading states, and caching, much like TanStack Query does.
So the real shift happening in the ecosystem isn't simply:
Redux → React Query
It looks more like this progression:
Manual API state in Redux
↓
Dedicated server-state solutions
↓
TanStack Query / RTK Query / Apollo / SWR
The broader industry is gradually waking up to the idea that server state and client state are fundamentally different responsibilities that deserve different tools.
Once you internalize that distinction, your overall state architecture becomes considerably easier to reason about.
The Rule I Now Use
When deciding whether a piece of data belongs in global state, one question does most of the work:
Who actually owns this data?
If the answer is:
The backend
you're almost certainly dealing with server state.
If the answer is:
The frontend
you're almost certainly dealing with client state.
That single question tends to point you toward a very different architecture depending on the case.
For instance:
Current user ────────── Server
Projects ────────────── Server
Orders ──────────────── Server
Notifications ───────── Server
Theme ───────────────── Client
Modal ───────────────── Client
Selected tab ────────── Client
Editor state ────────── Client
Once you lay it out that way, the right structure becomes obvious.
React Query Isn't Killing Redux
The claim that "React Query is replacing Redux" is a bit misleading as stated.
What's really changing is something more subtle:
Developers are getting better at identifying what category of state they're actually dealing with.
Redux used to be the default dumping ground for everything, server responses included.
Increasingly, teams are separating:
Server state
↓
TanStack Query
from:
Client state
↓
Redux / Zustand / Context / React
For a lot of modern React codebases, that separation alone eliminates a surprising share of the complexity that used to be blamed on Redux itself.
The point isn't to minimize how many libraries you use.
The point is to stop hand-building infrastructure for problems that already have solid, purpose-made abstractions.
So next time you come across a bloated Redux slice packed with API responses, loading flags, cache-invalidation logic, and refetch actions, ask yourself:
Do you actually need a global state manager for this, or have you just reimplemented React Query by hand inside Redux?
How Are You Handling This in Your Applications?
Do you currently keep server data inside Redux or Zustand, rely on TanStack Query, or take some other approach entirely?