Suggest an editImprove this articleRefine the answer for “What is RTK Query in Redux Toolkit?”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**RTK Query** is a data fetching and caching library built into Redux Toolkit. It auto-generates typed React hooks, handles loading states, and provides tag-based cache invalidation from a single `createApi` definition. No manual thunks or reducer cases needed. ```javascript const { data, isLoading } = useGetUserQuery(userId); // cached, auto-generated ``` **Key:** define endpoints once, RTK Query manages cache, refetching, and React integration.Shown above the full answer for quick recall.Answer (EN)Image**RTK Query** is a data fetching and caching library built into Redux Toolkit that auto-generates hooks, manages cache, and handles loading states from a single `createApi` call. ## Theory ### TL;DR - Think of it as a smart cache layer: define endpoints once, RTK Query handles fetch, store, and re-render automatically. - Main difference from plain Redux: no manual actions, reducers, or thunks. RTK Query generates all of that from your endpoint definitions. - Use it when your app has more than a few API endpoints. Skip it for pure client state (use `createSlice` instead). - Tags (`providesTags` / `invalidatesTags`) are the core mechanism for keeping data fresh after mutations. - Works in non-React apps too, but you lose the auto-generated hooks. ### Quick example ```javascript import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'; export const pokemonApi = createApi({ reducerPath: 'pokemonApi', baseQuery: fetchBaseQuery({ baseUrl: 'https://pokeapi.co/api/v2' }), endpoints: (builder) => ({ getPokemonByName: builder.query({ query: (name) => `pokemon/${name}`, // GET /pokemon/pikachu }), }), }); export const { useGetPokemonByNameQuery } = pokemonApi; // hook is ready ``` One `createApi` call produces a Redux slice, a middleware entry, and a typed React hook. No extra files needed. ### Key difference from manual Redux async With plain Redux, fetching data means writing an async thunk, action creators, and reducer cases for loading/success/error. RTK Query replaces all of that. You describe what data you need and the endpoint URL. RTK Query builds the cache slice, subscribes to it when the hook mounts, and removes the subscription when the component unmounts. The result is significantly less code with the same reliability. ### When to use - App has 5+ API endpoints: RTK Query cache beats hand-rolled thunks. - React app that already uses Redux: fits without adding another library. - Need polling, optimistic updates, or tag-based invalidation: all built in. - Pure client-side state with no server data: stick with `createSlice`. - Non-React app: use the RTK Query core API directly, without hooks. ### How it works internally RTK Query creates a dedicated Redux slice that stores responses normalized by query key. When a component calls `useGetPokemonByNameQuery('pikachu')`, the hook dispatches a subscription action. If the cache already has a fresh response for that key, it returns immediately. If not, it fires the `baseQuery` (a fetch wrapper). The response lands in the Redux store, and `useSyncExternalStore` triggers a re-render. Cache stays alive as long as at least one subscriber exists. When all components using a query unmount, RTK Query starts a countdown (60 seconds by default) and then drops the cached data. That is the garbage collection model. Tag-based invalidation runs the other direction. A mutation marks certain tags as stale. Any query that declared `providesTags` with those tags gets refetched on the next render. This is how adding to a cart can automatically refresh the product list without any manual dispatch. ### Auth tokens and headers ```javascript baseQuery: fetchBaseQuery({ baseUrl: '/api', prepareHeaders: (headers, { getState }) => { const token = getState().auth.token; if (token) headers.set('Authorization', `Bearer ${token}`); return headers; }, }) ``` `prepareHeaders` runs before every request. Reading from the Redux store here means the token stays current even after a refresh cycle. ### Common mistakes **1. Forgetting `providesTags` on list queries.** ```javascript // Wrong: mutations can never know to refetch this list getUsers: builder.query({ query: () => 'users' }) // Correct: tag each item so mutations can invalidate precisely getUsers: builder.query({ query: () => 'users', providesTags: (result) => result ? [...result.map(({ id }) => ({ type: 'User', id })), { type: 'User', id: 'LIST' }] : [{ type: 'User', id: 'LIST' }], }) ``` Without tags the cache stays stale after a mutation. This is the mistake I see most often in code review on RTK Query PRs. **2. Using manual `refetch()` instead of `invalidatesTags`.** ```javascript // Wrong: only refreshes this one query const [refetch] = useLazyGetPostQuery(); refetch(); // Correct: invalidates every query that depends on Post addPost: builder.mutation({ query: (body) => ({ url: 'posts', method: 'POST', body }), invalidatesTags: ['Post'], }) ``` Manual refetch breaks the dependency chain. Other queries that serve the same data stay stale. **3. Side effects inside `queryFn`.** ```javascript // Wrong: mutates localStorage in a function that may run in parallel queryFn: async () => { localStorage.clear(); return { data }; } ``` `baseQuery` can run in parallel for multiple endpoints. Side effects here cause race conditions and SSR mismatches. Keep `queryFn` pure. **4. Not using `skip` when the argument is not ready.** ```javascript // Wrong: fires a request with undefined in the URL const { data } = useGetUserQuery(userId); // Correct: wait until userId is available const { data } = useGetUserQuery(userId, { skip: !userId }); ``` RTK Query uses the argument as a cache key. `undefined` is valid but it fetches with `undefined` in the URL, which is almost never what you want. ### Real-world usage - E-commerce: product list and cart queries linked via tags (add to cart refetches stock automatically). - Admin dashboards: `pollingInterval: 30000` for live order counts without WebSockets. - React Native: RTK Query cache persisted with `redux-persist` for offline-first behavior. - Monorepos: single `createApi` instance shared across packages via `injectEndpoints`. - Auth flows: `prepareHeaders` injects JWT from Redux store on every request. ### Follow-up questions **Q:** How does tag invalidation work step by step? **A:** A mutation with `invalidatesTags: ['Post']` runs after the request resolves. RTK Query finds every query in the cache that declared `providesTags: ['Post']`, marks them stale, and refetches them on the next render cycle. **Q:** What is the difference between `query` and `queryFn`? **A:** `query` returns a URL string or config object that `baseQuery` wraps in a fetch call. `queryFn` gives you full control over the request, which is useful for GraphQL, WebSockets, or any non-HTTP data source. **Q:** How do you handle auth token refresh inside RTK Query? **A:** Wrap `fetchBaseQuery` in a custom `baseQueryWithReauth` function. Check for a 401, attempt a token refresh, update the store, and retry the original request. The Redux Toolkit docs call this pattern automatic re-authorization. **Q:** What happens to cache when a component unmounts? **A:** RTK Query starts a `keepUnusedDataFor` countdown (60 seconds by default). When it expires, the slice entry is removed. Set `keepUnusedDataFor: Infinity` on an endpoint to pin data permanently, or `0` to drop it immediately on unmount. **Q:** (Senior) How would you implement infinite scroll with RTK Query without unbounded memory growth? **A:** Use `serializeQueryArgs` to merge pages into a single cache key, `merge` to append incoming pages, and `forceRefetch` to detect page changes. For memory control, track loaded page numbers and evict old ones via `api.util.updateQueryData` or by invalidating tags for pages that scroll out of view. ## Examples ### Basic setup: users API with mutations ```javascript import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'; export const usersApi = createApi({ reducerPath: 'usersApi', baseQuery: fetchBaseQuery({ baseUrl: '/api' }), tagTypes: ['User'], endpoints: (builder) => ({ getUsers: builder.query({ query: () => 'users', providesTags: ['User'], }), updateUser: builder.mutation({ query: ({ id, ...data }) => ({ url: `users/${id}`, method: 'PUT', body: data, }), invalidatesTags: ['User'], // triggers getUsers refetch after update }), }), }); export const { useGetUsersQuery, useUpdateUserMutation } = usersApi; function UserList() { const { data, isLoading } = useGetUsersQuery(); const [updateUser] = useUpdateUserMutation(); if (isLoading) return <div>Loading...</div>; return ( <ul> {data?.map(user => ( <li key={user.id}> {user.name} <button onClick={() => updateUser({ id: user.id, name: 'New Name' })}> Rename </button> </li> ))} </ul> ); } ``` Renaming a user invalidates the `User` tag, so `getUsers` refetches automatically. No manual dispatch. No `useEffect` to sync state. ### Production pattern: e-commerce with granular tags ```javascript export const shopApi = createApi({ reducerPath: 'shopApi', baseQuery: fetchBaseQuery({ baseUrl: '/api' }), tagTypes: ['Product', 'Cart'], endpoints: (builder) => ({ getProducts: builder.query({ query: () => 'products', providesTags: (result) => result ? [ ...result.map(({ id }) => ({ type: 'Product', id })), { type: 'Product', id: 'LIST' }, ] : [{ type: 'Product', id: 'LIST' }], }), addToCart: builder.mutation({ query: ({ productId }) => ({ url: 'cart', method: 'POST', body: { productId }, }), invalidatesTags: ['Cart', { type: 'Product', id: 'LIST' }], }), }), }); ``` Granular tags mean adding one product to a cart refetches the product list but not every individual product detail page. This cuts unnecessary network traffic in high-traffic dashboards. ### Advanced: conditional fetch with polling and result transformation ```javascript function AdminPanel({ user }) { const { activeCount, isFetching } = useGetActiveOrdersQuery(undefined, { skip: !user?.isAdmin, // no request if not admin pollingInterval: user?.isAdmin ? 30000 : 0, // poll every 30s when active selectFromResult: ({ data, isFetching }) => ({ activeCount: data?.filter(o => o.status === 'active').length ?? 0, isFetching, }), }); return ( <div> Active orders: {activeCount} {isFetching && '(refreshing)'} </div> ); } ``` `skip: true` cancels the subscription entirely, which stops polling and prevents memory leaks. `selectFromResult` runs after the cache lookup, so the component only re-renders when `activeCount` changes, not on every polling response.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.