React Query vs Redux
Short answer
They are not alternatives. React Query manages server state — data that lives in a database and arrives over the network, with caching, refetching and invalidation handled for you. Redux manages client state your app owns outright. Most apps that felt they needed Redux were really storing server data in it.
| React Query | Redux (Toolkit) | |
|---|---|---|
| Manages | Server state — data you fetched | Client state — data you own |
| Source of truth | Your backend | The store in the browser |
| Caching | Built in, per query key | You write it |
| Refetch on focus / reconnect | Built in | You write it |
| Loading & error states | Returned by the hook | You track them yourself |
| Deduplicates parallel requests | Yes | No |
| Boilerplate for one endpoint | One hook | Slice, thunk, three action types |
| Good for | Lists, details, mutations | Cart, wizard, filters, auth UI |
The distinction that settles the question
Almost all confusion here comes from treating "state" as one thing. It is two. Client state is data your application invents and owns: whether a modal is open, what is in the cart, which tab is selected, what the user has typed. It exists only in the browser, you are the only writer, and it is correct by definition.
Server state is different in every important way. It lives in a database you do not control, other people can change it while you are looking at it, it arrives asynchronously and can fail, and the copy in your browser is a cache that starts going stale the moment it arrives. Treating that like client state — storing it in Redux and hoping — is the source of most of the complexity people blame on Redux.
| Client state | Server state |
|---|---|
| Is the sidebar open | The list of orders |
| Current step of a checkout wizard | The logged-in user's profile |
| Selected filters and sort order | Search results for those filters |
| Draft text in an unsaved form | The saved version of that record |
| Theme, locale, feature toggles | Product catalogue and prices |
What fetching in Redux actually costs
The classic pattern is a thunk that dispatches pending, fulfilled and rejected actions into a slice that tracks data, loading and error. It works, everyone has written it, and it is worth seeing next to the alternative — not because the code is bad, but because of everything it does not do.
1const fetchOrders = createAsyncThunk("orders/fetch", async (userId) => {
2 const res = await fetch(`/api/orders?user=${userId}`);
3 if (!res.ok) throw new Error("Failed");
4 return res.json();
5});
6
7const ordersSlice = createSlice({
8 name: "orders",
9 initialState: { data: [], status: "idle", error: null },
10 reducers: {},
11 extraReducers: (b) => {
12 b.addCase(fetchOrders.pending, (s) => { s.status = "loading"; })
13 .addCase(fetchOrders.fulfilled, (s, a) => { s.status = "ok"; s.data = a.payload; })
14 .addCase(fetchOrders.rejected, (s, a) => { s.status = "error"; s.error = a.error.message; });
15 },
16});
17
18function Orders({ userId }) {
19 const dispatch = useDispatch();
20 const { data, status } = useSelector((s) => s.orders);
21 useEffect(() => { dispatch(fetchOrders(userId)); }, [dispatch, userId]);
22 // → works, but: no caching, no dedupe, no refetch on focus,
23 // stale data from the previous userId shows during the new load
24}Now count what is missing. Two components mounting at once fire two identical requests. Navigating away and back refetches from scratch even though the data arrived four seconds ago. Switching `userId` shows the previous user's orders while the new ones load. Nothing retries a failed request. Nothing knows the data has gone stale. Every one of those is a real ticket someone eventually files.
1function Orders({ userId }) {
2 const { data, isPending, error } = useQuery({
3 queryKey: ["orders", userId],
4 queryFn: () => fetch(`/api/orders?user=${userId}`).then(r => r.json()),
5 });
6
7 if (isPending) return <Spinner />;
8 if (error) return <Error message={error.message} />;
9 return <List items={data} />;
10}
11// → caching per userId, dedupe across components, retry on failure,
12// refetch on window focus and reconnect, and no stale cross-user data.
13// The slice, the thunk and the three action types are all gone.The features you would have to build yourself
It is easy to look at the two snippets and conclude React Query is a syntax improvement. It is not. The difference is a set of behaviours that are individually simple and collectively a large amount of code you would otherwise own.
| Behaviour | What it means | In Redux you would… |
|---|---|---|
| Caching by key | Same key, same cached data | Write a normalised cache |
| Deduplication | Ten components, one request | Track in-flight promises |
| Stale-while-revalidate | Show cached, refetch quietly | Hand-roll timestamps |
| Refetch on focus | Data refreshes when you return | Add window listeners |
| Retry with backoff | Transient failures self-heal | Write retry logic |
| Pagination / infinite | Pages kept, not refetched | Manage page state manually |
| Optimistic updates | UI updates before the server replies | Write rollback logic |
| Garbage collection | Unused cache is dropped | Nothing — memory grows |
The refetch-on-focus behaviour is the one people notice first in real use. A user switches to another tab, comes back three minutes later, and the numbers are current without a reload. Implementing that once is easy; implementing it for every query with the right staleness rules is not.
This is 1 of 75+ questions in the React Interview Kit
Get every question with detailed answers, follow-ups and real code — plus coding challenges and a last-minute revision sheet. One-time payment, instant access.
⚡ Get the React Interview Kit → ₹399What Redux is still genuinely good at
None of this means Redux is finished. Once the server data moves out, what remains is client state — and for complex client state Redux is still an excellent tool, with a devtools experience nothing else matches. The point is that the remaining slice is much smaller than most apps assume.
1const cartSlice = createSlice({
2 name: "cart",
3 initialState: { lines: [], coupon: null },
4 reducers: {
5 add(state, { payload }) {
6 const line = state.lines.find(l => l.sku === payload.sku);
7 line ? line.qty += payload.qty : state.lines.push(payload);
8 },
9 applyCoupon(state, { payload }) { state.coupon = payload; },
10 clear(state) { state.lines = []; state.coupon = null; },
11 },
12});
13// → owned entirely by the client, several components read it,
14// the transitions are worth naming, and time-travel debugging
15// genuinely helps when a discount comes out wrong.The signals that a value belongs in a store are consistent: many unrelated components read it, the transitions are complex enough to deserve names, and you would benefit from seeing the sequence of changes when something goes wrong. A cart, a multi-step form, a canvas editor's undo stack and a permissions model all qualify. A list of products fetched from an API does not.
RTK Query: Redux's own answer
If your app already uses Redux Toolkit, RTK Query gives you most of React Query's behaviour inside the store you already have. It generates hooks from an endpoint definition, caches by cache key, deduplicates, and handles invalidation through a tag system. Choosing it over React Query is usually about what is already in the project rather than capability.
1export const api = createApi({
2 baseQuery: fetchBaseQuery({ baseUrl: "/api" }),
3 tagTypes: ["Order"],
4 endpoints: (build) => ({
5 getOrders: build.query({
6 query: (userId) => `orders?user=${userId}`,
7 providesTags: ["Order"],
8 }),
9 cancelOrder: build.mutation({
10 query: (id) => ({ url: `orders/${id}/cancel`, method: "POST" }),
11 invalidatesTags: ["Order"], // → the list refetches automatically
12 }),
13 }),
14});
15
16const { data, isLoading } = api.useGetOrdersQuery(userId);| Situation | Use |
|---|---|
| Already on Redux Toolkit | RTK Query — no second dependency |
| No store, or want to remove one | React Query |
| Want the smallest possible library | SWR |
| Next.js App Router, mostly server data | Server Components + fetch; add React Query for interactive parts |
| GraphQL | Apollo or urql — they cache at the field level |
| Complex client-only state | Redux Toolkit, Zustand or Jotai |
Do you still need Redux at all?
For many applications, honestly, no — and saying so with reasons is a better interview answer than defending it. The prop-drilling problem that made Redux essential in 2016 is largely handled by context, server state has moved to query libraries, and where a store is genuinely needed the lighter options are often a better fit for a small team.
Redux still wins on three things worth naming: the devtools, including time-travel debugging that no alternative matches; the middleware ecosystem, when you need to intercept every action for logging, analytics or persistence; and predictability at scale, where explicit named actions make a large codebase easier to reason about than scattered setters.
Using both together
The two coexist cleanly once the boundary is clear, and this is what most real migrations end up looking like. React Query owns everything that came from the network. Redux owns what the client invented. Neither copies the other's data, and the point where they meet is usually a mutation that also touches local state.
1function Checkout() {
2 const lines = useSelector(s => s.cart.lines); // client state
3 const dispatch = useDispatch();
4
5 const { data: address } = useQuery({ // server state
6 queryKey: ["address", "default"],
7 queryFn: getDefaultAddress,
8 });
9
10 const placeOrder = useMutation({
11 mutationFn: () => postOrder({ lines, addressId: address.id }),
12 onSuccess: (order) => {
13 dispatch(cartSlice.actions.clear()); // client state
14 queryClient.invalidateQueries({ queryKey: ["orders"] }); // server state
15 // → the orders list refetches; the cart empties. No duplicated data.
16 },
17 });
18
19 return <button onClick={() => placeOrder.mutate()}>Pay ₹{total(lines)}</button>;
20}What is the difference between React Query and SWR?
Both do stale-while-revalidate caching for server state. SWR is smaller and more opinionated; React Query has a larger feature surface — mutations with optimistic updates, infinite queries, a query cancellation model and much stronger devtools. For a simple read-heavy app SWR is plenty; for anything with substantial mutations React Query usually pays off.
Does React Query replace useEffect for data fetching?
Yes, and that is one of its main benefits. The useEffect-plus-useState fetch pattern has to handle race conditions, cleanup on unmount, refetch on parameter changes and error states by hand, and most hand-written versions get at least one of those wrong. A query hook handles all four.
How does React Query know when to refetch?
Data becomes stale after staleTime, which defaults to zero. Once stale, it refetches on window focus, on network reconnect, on component mount and when the query key changes. Every one of those triggers is configurable per query or globally, and setting a sensible staleTime is the first tuning most apps do.
Do I need React Query with Next.js Server Components?
Not for data you can fetch on the server and render once — an async Server Component with fetch is simpler and ships no client JavaScript. You still want it for anything interactive: data that refetches on focus, infinite lists, optimistic mutations. The two compose, with the server render providing the initial cached data.
Is Zustand a replacement for Redux?
For most client state, yes. It gives you a store with far less ceremony, no provider requirement and a much smaller bundle. What you give up is Redux's devtools depth and its middleware ecosystem. For a team that only ever used Redux to avoid prop drilling, Zustand is usually the better trade.
Frequently asked questions
- What is the difference between React Query and Redux?
- React Query manages server state — data fetched over the network, with caching, staleness, refetching and retries handled for you. Redux is a general client state container for data your app owns outright. They solve different problems, which is why most apps that use both keep server data out of the store entirely.
- Can React Query replace Redux?
- It replaces the part of Redux that was storing fetched data, which in many applications is most of the store. What remains — a cart, a wizard, filters, UI state — may be small enough for useState and context. If it is genuinely complex, keep a store for it.
- Should I use React Query or Redux for API calls?
- React Query, or RTK Query if you are already on Redux Toolkit. Writing API calls as thunks means implementing caching, deduplication, retries and refetching yourself, and most hand-written versions cover only some of them.
- What is the difference between server state and client state?
- Client state is owned by your app and is correct by definition: a modal being open, a form draft, the selected tab. Server state is a cached copy of data that lives elsewhere, can be changed by other people, arrives asynchronously and goes stale. Only server state needs caching and revalidation.
- What is the difference between React Query and RTK Query?
- They cover the same ground. RTK Query lives inside a Redux store and defines endpoints in one API slice with a tag-based invalidation system; React Query is standalone and organises everything around query keys. If you already use Redux Toolkit, RTK Query avoids a second dependency.
- Is Redux still worth learning in 2026?
- Yes, because a large share of existing production React runs on it and interviews still ask. What has changed is whether you would add it to a new project — usually not, unless you have complex client state that benefits from named actions, middleware and time-travel debugging.
- Can I use React Query and Redux together?
- Yes, and it is a common and clean arrangement. React Query owns everything fetched, Redux owns everything the client invented, and neither copies the other's data. They meet at mutations, where a successful write clears local state and invalidates the relevant queries.
- Does React Query store data in Redux?
- No. It has its own in-memory cache keyed by query key, with its own staleness and garbage-collection rules. That independence is the point: the cache knows things — how old the data is, whether a refetch is in flight — that a plain store has no concept of.
- What is the difference between staleTime and gcTime?
- staleTime is how long data is considered fresh; while fresh, React Query will not refetch it on focus or remount. gcTime, previously cacheTime, is how long unused data stays in memory after the last component using it unmounts. Fresh data can be garbage collected, and stale data can still be shown.
- How do optimistic updates work in React Query?
- In a mutation's onMutate you cancel in-flight queries, snapshot the current cache and write the expected result immediately so the UI responds at once. If the request fails, onError restores the snapshot; onSettled invalidates the query so the server's version wins in the end.
- Do I need Redux if I use Context?
- Often not. Context solves delivery — getting a value deep into the tree without prop drilling — and pairs fine with useReducer for the transitions. Its weakness is that every consumer re-renders when the value changes, so it suits values that change rarely. Frequently changing shared state is where a store earns its place.
- Is Zustand better than Redux?
- For most client state it is less code, has no provider requirement and a much smaller bundle. Redux keeps the advantage on devtools, the middleware ecosystem and the predictability of explicit named actions in a large codebase. For a small team with moderate state, Zustand is usually the better trade.
This is 1 of 75+ questions in the React Interview Kit
Get every question with detailed answers, follow-ups and real code — plus coding challenges and a last-minute revision sheet. One-time payment, instant access.
⚡ Get the React Interview Kit → ₹399