RTK Query and the alternatives
Server state in Redux, tags, and a comparison with TanStack Query, SWR, Apollo, RSC.
Updated
What RTK Query is
RTK Query is the part of Redux Toolkit that handles server state: it fetches data from an API, keeps it in a cache, deduplicates requests, manages loading/error and refreshes automatically after mutations. It's the equivalent of TanStack Query, but built on top of Redux.
The idea: you no longer write useEffect + fetch + useState for loading + a reducer for the data. You describe the endpoints once, and RTK Query generates the hooks.
A complete API
// shared/api/base-api.ts
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
export const api = createApi({
baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
tagTypes: ['Post'],
endpoints: build => ({
getPosts: build.query<Post[], { page: number }>({
query: ({ page }) => `posts?page=${page}`,
providesTags: result => [
...(result ?? []).map(p => ({ type: 'Post' as const, id: p.id })),
{ type: 'Post', id: 'LIST' },
],
}),
addPost: build.mutation<Post, NewPost>({
query: body => ({ url: 'posts', method: 'POST', body }),
invalidatesTags: [{ type: 'Post', id: 'LIST' }],
}),
}),
})
export const { useGetPostsQuery, useAddPostMutation } = api'use client'
const { data, isLoading, error } = useGetPostsQuery({ page })
const [addPost, { isLoading: saving }] = useAddPostMutation()Tags — how it knows what to refresh
| Concept | Role |
|---|---|
tagTypes |
the categories of data: 'Post', 'User' |
providesTags |
a query declares what data it contains: { type: 'Post', id: 1 } |
invalidatesTags |
a mutation declares what data it changed |
| the result | queries with invalidated tags are refetched automatically |
{ type: 'Post' } without an id invalidates every post; { type: 'Post', id: 1 } only post 1. It's the same principle as cacheTag / updateTag in Next and queryKey / invalidateQueries in TanStack Query.
The alternatives — a comparison
| RTK Query | TanStack Query | SWR | Apollo / urql | Server Components | |
|---|---|---|---|---|---|
| For | REST / anything | REST / anything | REST / anything | GraphQL | anything, on the server |
| Depends on | Redux | nothing | nothing | GraphQL | Next / RSC |
| Cache identified by | endpoint + arguments, tags | queryKey |
the key (URL) | type + id (normalized) | 'use cache' + cacheTag |
| Invalidation | tags, declarative | invalidateQueries |
mutate(key) |
automatic by type/id | updateTag / revalidateTag |
| Size | big (with Redux) | medium | small | big | 0 KB on the client |
| When | you already have Redux | the default on the client | simple needs, Vercel-style | a GraphQL API | the default in Next for display |
The practical recommendation:
- In the Next.js App Router, start with Server Components for displayed data and Server Actions for mutations.
- You need live data on the client (polling, infinite scroll, a dashboard) → TanStack Query.
- The project already uses Redux → RTK Query, so you don't end up with two systems.
- A GraphQL API → Apollo or urql.
Common mistakes
- Copying a query's result into a Redux slice or into
useState— two sources of truth. - Missing tags → after a mutation, the list doesn't update.
- A single
createApiper app; endpoints are added withinjectEndpointsfrom each feature.
Summary
- RTK Query = server state on top of Redux; endpoints → generated hooks.
providesTags+invalidatesTags= automatic refreshing after mutations.- Alternatives: TanStack Query (the default on the client), SWR (simple), Apollo (GraphQL), Server Components (Next).