The Developer's Quarterly · React Data FetchingVol. II · February 2026

SWR vs React Query vs RTK Query:Choosing the Right Data Fetching Library for Your React App

SWR, React Query, and RTK Query all solve server-state problems, but the right choice depends less on features than on app complexity, mutation patterns, and whether Redux already anchors the architecture.

React teams usually outgrow raw fetch() calls for the same reason: the hard part is not making a request, it is managing cache state, refetching, loading transitions, retries, background refreshes, and keeping the UI consistent when data changes.

That is where SWR, React Query and RTK Query come in. All three libraries solve server-state problems, but they optimize for different team constraints. One is deliberately minimal, one is feature-rich, and one fits best when Redux Toolkit is already part of the app architecture.

If you choose based only on popularity, you will probably over-install. The better question is simpler: how much control do you need, how much existing state infrastructure do you already have, and how opinionated do you want the data layer to be?

The shared problem they solve

Server state is different from local UI state because it lives on the server, can become stale at any time, and needs explicit rules for caching, refetching, invalidation, retries, and background synchronization.

1 Shared remote data

The same resource often feeds several screens or components, so one-off fetch logic becomes hard to coordinate.

2 Staleness rules

Data can be fresh, stale, invalidated, or being refreshed in the background, and the UI has to respond correctly to each case.

3 Async side effects

Focus changes, reconnects, mutations, polling, and route transitions all create refetch behavior that becomes tedious to hand-roll.

SWR: the lightweight option

SWR is built around the stale-while-revalidate model: return cached data immediately, then refresh it in the background. That makes it feel fast and intentionally small.

SWR basic query
import useSWR from 'swr'; const fetcher = (url: string) => fetch(url).then((res) => res.json()); export function UserCard() { const { data, error, isLoading } = useSWR('/api/user', fetcher); if (isLoading) return <p>Loading...</p>; if (error) return <p>Request failed.</p>; return <p>Signed in as {data.name}</p>; }

SWR handles caching, deduplication, focus revalidation, interval polling, and optimistic update helpers without asking the team to adopt a heavy client abstraction. That makes it a strong fit for read-heavy interfaces such as dashboards, profile screens, settings pages, and internal tools.

React Query: the most configurable toolkit

React Query, now distributed as @tanstack/react-query, is usually the best choice when server-state workflows become more operationally complex. It gives you cache lifetimes, invalidation, pagination primitives, dependent queries, optimistic updates, and a richer mutation lifecycle.

React Query basic setup
import { QueryClient, QueryClientProvider, useQuery, } from '@tanstack/react-query'; const queryClient = new QueryClient(); function ProjectsList() { const { data, isPending, error } = useQuery({ queryKey: ['projects'], queryFn: async () => { const response = await fetch('/api/projects'); if (!response.ok) { throw new Error('Failed to load projects'); } return response.json(); }, staleTime: 60_000, }); if (isPending) return <p>Loading projects...</p>; if (error) return <p>Unable to load projects.</p>; return data.map((project) => <div key={project.id}>{project.name}</div>); } export function App() { return ( <QueryClientProvider client={queryClient}> <ProjectsList /> </QueryClientProvider> ); }

If SWR feels like a clean convenience layer, React Query feels like a full server-state toolkit. That is its advantage, but it also means more API surface and more decisions for the team.

RTK Query: the Redux-native choice

RTK Query is the data fetching layer built into Redux Toolkit. Instead of attaching fetch logic to individual hooks, you define endpoints once inside an API slice and consume generated hooks throughout the app.

RTK Query API slice
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'; export const api = createApi({ reducerPath: 'api', baseQuery: fetchBaseQuery({ baseUrl: '/api' }), tagTypes: ['Post'], endpoints: (builder) => ({ getPosts: builder.query({ query: () => '/posts', providesTags: ['Post'], }), }), }); export const { useGetPostsQuery } = api;
RTK Query generated hook
export function Posts() { const { data = [], isLoading } = useGetPostsQuery(); if (isLoading) return <p>Loading posts...</p>; return ( <ul> {data.map((post) => ( <li key={post.id}>{post.title}</li> ))} </ul> ); }

RTK Query is compelling when Redux already anchors the application. It keeps data fetching, cache invalidation, and the rest of the state layer inside one shared architecture. If Redux is not already present, adding it only to fetch data is harder to justify.

Comparison at a glance

Dimension SWR React Query RTK Query
Mental model Small hook plus cache key Query client with rich policies Central API slice in Redux
Best fit Read-heavy apps with modest complexity Complex async workflows and mutations Redux Toolkit applications
Mutation ergonomics Good but lighter-weight Very strong lifecycle and invalidation support Strong via endpoints and tag invalidation
Boilerplate Low Moderate Moderate upfront, lower per endpoint later
Redux dependency None None Yes

Mutations are often the deciding factor

Most teams can make any of these libraries work for simple reads. The decision usually becomes obvious once writes, invalidation, and optimistic updates enter the picture.

SWR mutation
import { mutate } from 'swr'; async function renameUser(name: string) { await fetch('/api/user', { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name }), }); await mutate('/api/user'); }
React Query mutation
import { useMutation, useQueryClient } from '@tanstack/react-query'; function RenameButton() { const queryClient = useQueryClient(); const renameMutation = useMutation({ mutationFn: async (name: string) => { const response = await fetch('/api/user', { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name }), }); if (!response.ok) { throw new Error('Rename failed'); } }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['user'] }); }, }); return <button onClick={() => renameMutation.mutate('Mauro')}>Rename user</button>; }
RTK Query mutation endpoint
updatePost: builder.mutation({ query: (body) => ({ url: '/posts/' + body.id, method: 'PATCH', body, }), invalidatesTags: ['Post'], });

SWR keeps writes lightweight, React Query gives you the richest lifecycle around them, and RTK Query makes invalidation rules scale well by centralizing them near endpoint definitions.

Choose by team context, not hype

Choose SWR Best when you want fast adoption, low boilerplate, and mostly read-oriented remote data.
Choose React Query Best when the app has multiple mutations, cache edge cases, pagination, or dependent query flows.
Choose RTK Query Best when Redux Toolkit is already foundational and a centralized API slice improves consistency.
Avoid over-architecture Do not add Redux just for RTK Query, and do not force a minimal tool into a workflow that clearly needs stronger mutation control.

Bottom line

If you want the shortest practical guidance, start with SWR for simple to medium complexity apps, move to React Query when the async workflow becomes operationally complex, and choose RTK Query when Redux Toolkit is already a core architectural decision.

There is no universal winner because the libraries are not optimizing for the same target. The right choice is the one that makes your app's server-state workflow feel smaller, not bigger.