EmpireUI
Get Pro
← Blog9 min read#tanstack query#redux#state management

TanStack Query vs Redux Toolkit: Server State vs Global State

TanStack Query and Redux Toolkit solve completely different problems. Here's how to pick the right tool — or use both — without over-engineering your React app.

React state management code on a dark monitor screen

The Comparison That Shouldn't Exist (But Here We Are)

People keep putting TanStack Query and Redux Toolkit in the same bracket, and it keeps causing confusion. They're not competitors. One manages *server state* — data that lives on a remote server and gets fetched, cached, and synced — and the other manages *client state* — UI flags, user preferences, wizard steps, anything that only exists in memory. Comparing them is a bit like arguing whether a wrench or a screwdriver is better. Wrong question.

That said, the confusion is understandable. Before TanStack Query (then called React Query) hit v3 in 2020, teams were stuffing API responses into Redux stores and writing thunks for every endpoint. Redux became the de-facto everything bucket. It worked, but the boilerplate was painful and the caching logic was either wrong or non-existent. TanStack Query showed up and made async data fetching trivially easy, which led a lot of developers to wonder: do I still need Redux at all?

Honestly, the answer for most apps is no — not for server data. But that doesn't mean Redux Toolkit is dead. It just means you should stop using it for things it was never designed to do. This article breaks down the real difference, when you'd reach for each, and what it actually looks like to use them together without making a mess.

What TanStack Query Actually Solves

TanStack Query (v5 as of late 2024) is a server-state library. Its job is to fetch data, cache it, keep it fresh, and handle loading/error states automatically. You write a useQuery hook, hand it a key and a fetch function, and it handles the rest. Background refetching on window focus. Stale-while-revalidate. Automatic retries. Pagination. Infinite scroll. Optimistic updates. All of it baked in.

import { useQuery } from '@tanstack/react-query';

interface User {
  id: number;
  name: string;
  email: string;
}

function UserProfile({ userId }: { userId: number }) {
  const { data, isLoading, error } = useQuery<User>({
    queryKey: ['user', userId],
    queryFn: () => fetch(`/api/users/${userId}`).then(r => r.json()),
    staleTime: 1000 * 60 * 5, // 5 minutes
  });

  if (isLoading) return <Skeleton />;
  if (error) return <ErrorState />;

  return <div>{data.name}</div>;
}

The queryKey is the clever part. TanStack Query uses it to deduplicate requests — if three components on the same page call useQuery({ queryKey: ['user', 42] }), only one network request fires. The cache entry is shared automatically. You didn't have to write a single line of Redux or Context to make that happen.

Worth noting: mutation support via useMutation is equally strong. You can optimistically update a list, handle rollback on failure, and invalidate related queries on success — all in about 20 lines. It's the kind of thing that used to require a whole Redux slice plus a thunk plus manual cache invalidation logic sprinkled across multiple files.

In practice, TanStack Query eliminates the 80% of Redux stores that were just API response caches with some loading booleans stapled on. If your state lives on a server and gets fetched over HTTP, TanStack Query owns that problem.

What Redux Toolkit Actually Solves

Redux Toolkit (RTK) is the modern way to write Redux — no more hand-rolling action constants, switch statements, or combineReducers by hand. Since RTK 2.0 shipped in late 2023, setup is fast and the API is genuinely pleasant. But what's it *for* now that TanStack Query handles server state?

RTK is for client state that multiple distant components need to share. Authentication state (is the user logged in, what's their role?). Multi-step form data that persists across routes. Feature flags toggled by the user. A global notification queue. A shopping cart that doesn't need a server round-trip to update. Anything with complex state transitions that benefit from deterministic reducers and time-travel debugging.

// features/auth/authSlice.ts
import { createSlice, PayloadAction } from '@reduxjs/toolkit';

interface AuthState {
  userId: string | null;
  role: 'admin' | 'user' | 'guest';
  isAuthenticated: boolean;
}

const initialState: AuthState = {
  userId: null,
  role: 'guest',
  isAuthenticated: false,
};

const authSlice = createSlice({
  name: 'auth',
  initialState,
  reducers: {
    login(state, action: PayloadAction<{ userId: string; role: AuthState['role'] }>) {
      state.userId = action.payload.userId;
      state.role = action.payload.role;
      state.isAuthenticated = true;
    },
    logout(state) {
      return initialState;
    },
  },
});

export const { login, logout } = authSlice.actions;
export default authSlice.reducer;

RTK also ships RTK Query — its own data-fetching solution — which is worth mentioning. It's excellent if you're already deep in Redux and want one coherent system. But if you're starting fresh or your team already knows TanStack Query, RTK Query adds a layer you probably don't need. Pick one data-fetching solution and stick with it.

Look, the Redux DevTools alone are worth keeping RTK for complex client state. Being able to replay state transitions, inspect every action, and time-travel through your app's history is invaluable when you're debugging a gnarly multi-step checkout flow at 11pm.

Using Both Together Without Losing Your Mind

The most pragmatic setup for a serious React app in 2026 is TanStack Query for server state plus RTK for client state. They coexist without any friction — they don't even know each other exists. Your query client manages the cache, your Redux store manages the UI layer.

// App.tsx — wiring both up
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { Provider } from 'react-redux';
import { store } from './store';

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 1000 * 60 * 2, // 2 minutes default
      retry: 2,
    },
  },
});

export function App() {
  return (
    <Provider store={store}>
      <QueryClientProvider client={queryClient}>
        <Router />
      </QueryClientProvider>
    </Provider>
  );
}

A real-world pattern: after a successful login mutation, you dispatch a Redux action to set auth state *and* call queryClient.invalidateQueries() to refetch anything that's now user-specific. The two systems communicate at the seam of your business logic, not at the library level.

function useLogin() {
  const dispatch = useAppDispatch();
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: (credentials: Credentials) =>
      authApi.login(credentials),
    onSuccess(data) {
      // RTK handles the auth state
      dispatch(login({ userId: data.userId, role: data.role }));
      // TanStack Query clears stale user-specific caches
      queryClient.invalidateQueries({ queryKey: ['user'] });
    },
  });
}

Quick aside: if your app is small — one developer, a handful of routes, no complex UI state — you can often get away with TanStack Query alone and useState/useContext for the client state. Don't add RTK just because a blog post told you to. Architecture should follow complexity, not precede it.

Performance: Who Wins and Where

TanStack Query's caching is legitimately good out of the box. By default, cached data is considered fresh for 0 seconds (immediately stale) — which sounds aggressive, but background refetches happen silently and users see the cached data instantly. Set staleTime to something sensible for your data (I use 60,000ms for reference data, 0 for anything user-generated) and you get snappy UIs with minimal over-fetching.

Redux doesn't do any network requests, so the performance question there is about re-render efficiency. RTK uses Immer under the hood, so your reducers write mutative-looking code that produces new references correctly. Pair it with createSelector from Reselect (bundled with RTK) and you get memoized selectors that prevent unnecessary re-renders even in components subscribed to big state slices.

// Memoized selector — only re-renders when role changes
import { createSelector } from '@reduxjs/toolkit';

const selectAuth = (state: RootState) => state.auth;

export const selectUserRole = createSelector(
  selectAuth,
  (auth) => auth.role
);

One more thing — TanStack Query v5 dropped the deprecated cacheTime rename confusion and aligned the API significantly. If you're still on v4 (or heaven forbid v3 from 2021), upgrade. The v5 object-syntax API is cleaner and the bundle size improvements are real.

When You Might Not Need Either

Next.js 13+ App Router changes the equation a bit. React Server Components fetch data on the server and stream it to the client — no useQuery needed for the initial render. TanStack Query still earns its place for client-side interactions after the page loads (mutations, background syncs, user-triggered refetches), but you might be fetching a lot less from the client than you think.

Similarly, if your only global state is a theme preference and whether a sidebar is open, useContext with a useReducer is completely fine. Pulling in RTK for that is like buying a truck to carry groceries. The 48kb gzipped cost isn't catastrophic, but it's also not free.

For Empire UI projects specifically — whether you're building with glassmorphism components or one of the industry templates — the majority of UI state (open/closed panels, active tabs, selected style variants) lives locally. TanStack Query handles the backend bits, useState handles the rest, and RTK only enters the picture if you've genuinely got cross-route shared state that can't be solved simpler.

The react-query-vs-swr article on the blog covers the TanStack Query vs SWR trade-off if you're also evaluating SWR — similar philosophy, different trade-offs on bundle size and features.

The Decision Framework (Practical, Not Theoretical)

Here's how to actually decide. Ask three questions. First: is this data coming from a server? Yes → TanStack Query. Second: does this state need to be shared across routes or deeply nested components with no common parent? Yes → RTK. Third: is it just local component state? Yes → useState or useReducer and move on.

The mistake most teams make is defaulting to Redux for everything because they're familiar with it, then being surprised when they're writing 200 lines of boilerplate to load a user profile. Or defaulting to TanStack Query for everything and then stuffing localStorage-synced auth tokens into a query cache in a way that doesn't quite work. The abstraction mismatch causes pain.

You can browse Empire UI's components and pick up full-stack patterns that already make these decisions for you — the dashboard templates wire TanStack Query for data and keep UI state local. It's a good reference if you want to see what a clean production split actually looks like without having to reverse-engineer it from scratch.

State management is one of those areas where the answer genuinely is "it depends" — but what it depends *on* is much simpler than most articles admit. Server state or client state. That's the fork. Everything else flows from there.

FAQ

Can I use TanStack Query and Redux Toolkit in the same React project?

Yes, and for non-trivial apps you probably should. TanStack Query owns server state (API responses, caching, background sync) while RTK owns client state (auth, UI flags, cross-route shared data). They don't conflict — just wrap your app in both providers.

Does TanStack Query replace Redux entirely?

It replaces the parts of Redux that were just API response caches with loading booleans. If you're only using Redux to store fetched data, yes — TanStack Query does that better. But RTK is still the right tool for complex client-only state shared across distant components.

What's the difference between RTK Query and TanStack Query?

RTK Query is Redux Toolkit's built-in data-fetching solution that integrates tightly with the Redux store. TanStack Query is standalone and framework-agnostic. Both are solid; pick TanStack Query if you're not already invested in Redux, RTK Query if you want one coherent Redux ecosystem.

Should I use TanStack Query with Next.js App Router?

For initial data fetching, React Server Components handle that server-side so you may not need TanStack Query at all. It still earns its place for client-side mutations, background refetches after user interactions, and infinite scroll — anything that happens post-hydration.

Free components in 40 styles
React & Tailwind, copy-paste ready.
Browse →

Read next

TanStack Query vs SWR in 2026: Data Fetching Library ShowdownZustand vs Jotai in 2026: Atomic vs Single Store StateTanStack Query v5 in React: Data Fetching That Actually ScalesZustand in React: Simple State Management That Gets Out of Your Way