Back to Blog

React Query vs Redux Toolkit: When to Use Each

Choosing between React Query and Redux Toolkit can be confusing for beginners. They both manage state, but they solve different problems. In this guide, you'll learn exactly when to use each tool, with clear examples and practical advice. Let's end the confusion!

The Core Difference

Here's the key insight that will guide all your decisions:

  • React Query (TanStack Query) - Manages server state (data from APIs)
  • Redux Toolkit - Manages client state (UI state, user preferences)

Understanding this distinction is crucial. Server state has unique challenges: caching, synchronization, background updates, and staleness. Client state is simpler: it's local to your app and doesn't need syncing with a server.

What is React Query?

React Query is a data-fetching library that makes working with server state effortless. It handles caching, background updates, and synchronization automatically.

React Query Example

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

// Fetching data
function UserProfile({ userId }) {
  const { data, isLoading, error, isError } = useQuery({
    queryKey: ['user', userId],
    queryFn: async () => {
      const response = await fetch(`/api/users/${userId}`);
      if (!response.ok) throw new Error('Failed to fetch');
      return response.json();
    },
    staleTime: 5 * 60 * 1000, // Consider data fresh for 5 minutes
    cacheTime: 10 * 60 * 1000, // Keep in cache for 10 minutes
    retry: 3, // Retry failed requests 3 times
  });

  if (isLoading) return <div>Loading...</div>;
  if (isError) return <div>Error: {error.message}</div>;

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

// Updating data
function UpdateProfile() {
  const queryClient = useQueryClient();
  
  const mutation = useMutation({
    mutationFn: async (newData) => {
      const response = await fetch('/api/user', {
        method: 'PUT',
        body: JSON.stringify(newData),
      });
      return response.json();
    },
    onSuccess: () => {
      // Invalidate and refetch user queries
      queryClient.invalidateQueries({ queryKey: ['user'] });
    },
  });

  const handleUpdate = () => {
    mutation.mutate({ name: 'John Doe' });
  };

  return (
    <button onClick={handleUpdate} disabled={mutation.isLoading}>
      {mutation.isLoading ? 'Updating...' : 'Update Profile'}
    </button>
  );
}

React Query Pros:

  • ✅ Automatic caching and background refetching
  • ✅ Loading and error states built-in
  • ✅ Optimistic updates made easy
  • ✅ Automatic request deduplication
  • ✅ Pagination and infinite scroll support
  • ✅ Much less boilerplate code
  • ✅ DevTools for debugging queries

React Query Cons:

  • ❌ Only handles server state
  • ❌ Learning curve for query keys
  • ❌ Adds another dependency

What is Redux Toolkit?

Redux Toolkit is the modern way to write Redux. It simplifies Redux by providing opinionated utilities for common patterns like creating reducers, actions, and stores.

Redux Toolkit Example

import { createSlice, configureStore } from '@reduxjs/toolkit';
import { useSelector, useDispatch } from 'react-redux';

// Create a slice
const themeSlice = createSlice({
  name: 'theme',
  initialState: {
    mode: 'light',
    sidebarOpen: false,
    notifications: [],
  },
  reducers: {
    toggleTheme: (state) => {
      state.mode = state.mode === 'light' ? 'dark' : 'light';
    },
    toggleSidebar: (state) => {
      state.sidebarOpen = !state.sidebarOpen;
    },
    addNotification: (state, action) => {
      state.notifications.push(action.payload);
    },
    removeNotification: (state, action) => {
      state.notifications = state.notifications.filter(
        (n) => n.id !== action.payload
      );
    },
  },
});

export const { 
  toggleTheme, 
  toggleSidebar, 
  addNotification, 
  removeNotification 
} = themeSlice.actions;

// Configure store
const store = configureStore({
  reducer: {
    theme: themeSlice.reducer,
  },
});

// Using in components
function ThemeToggle() {
  const mode = useSelector((state) => state.theme.mode);
  const dispatch = useDispatch();

  return (
    <button onClick={() => dispatch(toggleTheme())}>
      Current theme: {mode}
    </button>
  );
}

function Sidebar() {
  const isOpen = useSelector((state) => state.theme.sidebarOpen);
  const dispatch = useDispatch();

  return (
    <aside className={isOpen ? 'open' : 'closed'}>
      <button onClick={() => dispatch(toggleSidebar())}>
        Toggle Sidebar
      </button>
    </aside>
  );
}

Redux Toolkit Pros:

  • ✅ Predictable state management
  • ✅ Great for complex client state
  • ✅ Time-travel debugging with DevTools
  • ✅ Immer for immutable updates
  • ✅ Middleware ecosystem (sagas, thunks)
  • ✅ Server state support via RTK Query

Redux Toolkit Cons:

  • ❌ More boilerplate than React Query for API calls
  • ❌ Steeper learning curve
  • ❌ Can be overkill for simple apps
  • ❌ Requires understanding of Redux concepts

When to Use React Query

Choose React Query when you're primarily dealing with server data:

✅ Perfect Use Cases for React Query:

  • Fetching user profiles - Needs caching and background updates
  • Product listings - Benefits from pagination and infinite scroll
  • Dashboard data - Requires automatic refetching
  • Search results - Needs request deduplication
  • Real-time data - Benefits from polling and window focus refetching
  • CRUD operations - Mutations with optimistic updates

Real-World React Query Example

// Infinite scroll with React Query
import { useInfiniteQuery } from '@tanstack/react-query';

function ProductList() {
  const {
    data,
    fetchNextPage,
    hasNextPage,
    isFetchingNextPage,
  } = useInfiniteQuery({
    queryKey: ['products'],
    queryFn: async ({ pageParam = 1 }) => {
      const response = await fetch(`/api/products?page=${pageParam}`);
      return response.json();
    },
    getNextPageParam: (lastPage, pages) => {
      return lastPage.hasMore ? pages.length + 1 : undefined;
    },
  });

  return (
    <div>
      {data?.pages.map((page) => (
        page.products.map((product) => (
          <ProductCard key={product.id} product={product} />
        ))
      ))}
      
      {hasNextPage && (
        <button 
          onClick={() => fetchNextPage()}
          disabled={isFetchingNextPage}
        >
          {isFetchingNextPage ? 'Loading...' : 'Load More'}
        </button>
      )}
    </div>
  );
}

When to Use Redux Toolkit

Choose Redux Toolkit when you need complex client-side state management:

✅ Perfect Use Cases for Redux Toolkit:

  • Theme preferences - Global UI state
  • User authentication - Token and user info across app
  • Shopping cart - Complex client state with calculations
  • Multi-step forms - State persisted across steps
  • UI state - Sidebar open/closed, modals, notifications
  • Complex filtering - Multiple filter criteria to manage

Real-World Redux Toolkit Example

import { createSlice } from '@reduxjs/toolkit';

// Shopping cart with complex logic
const cartSlice = createSlice({
  name: 'cart',
  initialState: {
    items: [],
    coupon: null,
    shipping: 0,
  },
  reducers: {
    addToCart: (state, action) => {
      const existingItem = state.items.find(
        (item) => item.id === action.payload.id
      );
      
      if (existingItem) {
        existingItem.quantity += 1;
      } else {
        state.items.push({ ...action.payload, quantity: 1 });
      }
    },
    removeFromCart: (state, action) => {
      state.items = state.items.filter(
        (item) => item.id !== action.payload
      );
    },
    updateQuantity: (state, action) => {
      const item = state.items.find((i) => i.id === action.payload.id);
      if (item) {
        item.quantity = action.payload.quantity;
      }
    },
    applyCoupon: (state, action) => {
      state.coupon = action.payload;
    },
    setShipping: (state, action) => {
      state.shipping = action.payload;
    },
  },
});

// Selectors with complex calculations
export const selectCartTotal = (state) => {
  const subtotal = state.cart.items.reduce(
    (sum, item) => sum + item.price * item.quantity,
    0
  );
  
  const discount = state.cart.coupon 
    ? subtotal * state.cart.coupon.percent / 100 
    : 0;
  
  return subtotal - discount + state.cart.shipping;
};

export const selectItemCount = (state) => {
  return state.cart.items.reduce(
    (sum, item) => sum + item.quantity,
    0
  );
};

Can You Use Both Together?

Absolutely! In fact, using both is often the best approach:

  • React Query for all server state (API calls, data fetching)
  • Redux Toolkit for client state (UI state, cart, preferences)

Combined Example

// React Query for products (server state)
function ProductList() {
  const { data: products } = useQuery({
    queryKey: ['products'],
    queryFn: fetchProducts,
  });

  // Redux for cart (client state)
  const dispatch = useDispatch();
  const cartItems = useSelector((state) => state.cart.items);

  const handleAddToCart = (product) => {
    dispatch(addToCart(product));
  };

  return (
    <div>
      {products?.map((product) => (
        <div key={product.id}>
          <h3>{product.name}</h3>
          <button onClick={() => handleAddToCart(product)}>
            Add to Cart
          </button>
        </div>
      ))}
    </div>
  );
}

RTK Query: Redux's Answer to React Query

Redux Toolkit includes RTK Query, which brings React Query-like features to Redux:

import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';

const api = createApi({
  baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
  endpoints: (builder) => ({
    getUsers: builder.query({
      query: () => 'users',
    }),
    getUserById: builder.query({
      query: (id) => `users/${id}`,
    }),
    updateUser: builder.mutation({
      query: ({ id, ...patch }) => ({
        url: `users/${id}`,
        method: 'PUT',
        body: patch,
      }),
    }),
  }),
});

export const { useGetUsersQuery, useGetUserByIdQuery, useUpdateUserMutation } = api;

// Usage
function UserList() {
  const { data, isLoading, error } = useGetUsersQuery();
  
  if (isLoading) return <div>Loading...</div>;
  if (error) return <div>Error!</div>;
  
  return <ul>{data.map((user) => <li>{user.name}</li>)}</ul>;
}

Decision Tree: Which Should You Use?

Question Answer Use
Is it data from an API? Yes React Query
Is it UI state (theme, sidebar)? Yes Redux Toolkit
Does it need caching? Yes React Query
Does it persist across pages? Yes Redux Toolkit
Complex calculations on state? Yes Redux Toolkit
Already using Redux? Yes Consider RTK Query

Common Mistakes to Avoid

❌ Using Redux for API Calls

// Bad - Complex and error-prone
const [data, setData] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);

useEffect(() => {
  setLoading(true);
  fetch('/api/data')
    .then(res => res.json())
    .then(data => { setData(data); setLoading(false); })
    .catch(err => { setError(err); setLoading(false); });
}, []);

// Good - Use React Query
const { data, isLoading, error } = useQuery({
  queryKey: ['data'],
  queryFn: fetchData,
});

❌ Using React Query for UI State

// Bad - Misusing React Query
const { data: sidebarOpen } = useQuery({
  queryKey: ['sidebar'],
  queryFn: () => false, // Not server data!
});

// Good - Use local state or Redux
const [sidebarOpen, setSidebarOpen] = useState(false);
// or
const sidebarOpen = useSelector((state) => state.ui.sidebarOpen);

Quick Setup Guide

React Query Setup

npm install @tanstack/react-query

// App.jsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 1 * 60 * 1000, // 1 minute
      cacheTime: 5 * 60 * 1000, // 5 minutes
      retry: 1,
      refetchOnWindowFocus: false,
    },
  },
});

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <YourApp />
    </QueryClientProvider>
  );
}

Redux Toolkit Setup

npm install @reduxjs/toolkit react-redux

// store.js
import { configureStore } from '@reduxjs/toolkit';
import cartReducer from './cartSlice';
import themeReducer from './themeSlice';

export const store = configureStore({
  reducer: {
    cart: cartReducer,
    theme: themeReducer,
  },
});

// App.jsx
import { Provider } from 'react-redux';
import { store } from './store';

function App() {
  return (
    <Provider store={store}>
      <YourApp />
    </Provider>
  );
}

Resources for Learning More

  • TanStack Query Docs - Official React Query documentation
  • Redux Toolkit Docs - Modern Redux guide
  • RTK Query - Redux's data fetching solution
  • React Query vs Redux - Official comparison guide

Key Takeaways

  • React Query is for server state (API data)
  • Redux Toolkit is for client state (UI state)
  • You can (and often should) use both together
  • React Query reduces boilerplate for data fetching
  • Redux is better for complex client-side logic
  • RTK Query bridges the gap if you're already using Redux
  • Choose based on the type of state, not popularity

Stop worrying about which is "better"—they solve different problems! Use React Query for server data, Redux Toolkit for complex client state, and enjoy the best of both worlds. Your code will be cleaner, more maintainable, and easier to understand.

💡 Pro tip: Start with React Query for all your API calls. Only add Redux if you find yourself prop-drilling complex client state through multiple components!