Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Що таке RTK Query в Redux Toolkit?». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**RTK Query** - бібліотека для отримання даних і кешування, вбудована в Redux Toolkit. Автоматично генерує типізовані React-хуки, відстежує стани завантаження та інвалідацію кешу за тегами з одного виклику `createApi`. Ручні thunks і reducers не потрібні. ```javascript const { data, isLoading } = useGetUserQuery(userId); // кешується, хук автогенерований ``` **Ключове:** описуєш ендпоінти один раз, RTK Query бере на себе кеш, рефетч і React-інтеграцію.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**RTK Query** - це бібліотека для отримання даних і кешування, вбудована в Redux Toolkit, яка автоматично генерує хуки, управляє кешем і відстежує стани завантаження з одного виклику `createApi`. ## Теорія ### TL;DR - Уяви розумний кеш-шар: описуєш ендпоінти один раз, RTK Query сам робить fetch, зберігає в store і оновлює компоненти. - Головна відмінність від звичайного Redux: ніяких ручних actions, reducers чи thunks. RTK Query генерує все це з твоїх endpoint-визначень. - Використовуй, якщо в додатку більше кількох API-ендпоінтів. Для чистого клієнтського стану без серверних даних бери `createSlice`. - Теги (`providesTags` / `invalidatesTags`) - основний механізм актуальності даних після мутацій. - Працює і без React, але тоді немає автогенерованих хуків. ### Швидкий приклад ```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; // хук готовий ``` Один виклик `createApi` генерує Redux-слайс, запис у middleware і типізований React-хук. Жодних додаткових файлів. ### Головна відмінність від ручного Redux async З чистим Redux отримання даних означає asyncThunk, action creators і reducer-кейси для loading/success/error. RTK Query замінює все це. Описуєш які дані потрібні і URL ендпоінту. RTK Query будує кеш-слайс, підписується на нього коли хук монтується, і прибирає підписку коли компонент анмонтується. Менше коду, та сама надійність. ### Коли використовувати - 5+ API-ендпоінтів: кеш RTK Query виграє у порівнянні з ручними thunks. - React-додаток, що вже використовує Redux: RTK Query вписується без нової залежності. - Потрібен polling, оптимістичні оновлення або інвалідація за тегами: все вбудовано. - Тільки клієнтський стан без серверних даних: залишайся на `createSlice`. - Не-React додаток: використовуй RTK Query core API напряму, без хуків. ### Як це працює всередині RTK Query створює окремий Redux-слайс, де зберігає відповіді нормалізовано за ключем запиту. Коли компонент викликає `useGetPokemonByNameQuery('pikachu')`, хук диспатчить дію підписки. Якщо в кеші вже є свіжа відповідь для цього ключа, хук повертає її одразу. Якщо ні, запускається `baseQuery` (обгортка навколо fetch). Відповідь потрапляє в Redux-store, і `useSyncExternalStore` тригерить ре-рендер. Кеш живе, поки є хоча б один підписник. Коли всі компоненти, що використовують запит, анмонтуються, RTK Query запускає відлік (60 секунд за замовчуванням), після якого видаляє закешовані дані. Це модель збирання сміття. Інвалідація (invalidation) за тегами працює в зворотному напрямку. Мутація позначає певні теги як застарілі. Будь-який запит, що оголосив `providesTags` з цими тегами, отримує рефетч при наступному рендері. Саме так додавання до кошика може автоматично оновлювати список продуктів без жодного ручного dispatch. ### Auth-токени і заголовки ```javascript baseQuery: fetchBaseQuery({ baseUrl: '/api', prepareHeaders: (headers, { getState }) => { const token = getState().auth.token; if (token) headers.set('Authorization', `Bearer ${token}`); return headers; }, }) ``` `prepareHeaders` запускається перед кожним запитом. Читання з Redux-store тут означає, що токен залишається актуальним навіть після рефрешу. ### Типові помилки **1. Не вказувати `providesTags` у list-запитах.** ```javascript // Неправильно: мутації не знають, що потрібно рефетчити getUsers: builder.query({ query: () => 'users' }) // Правильно: тегуємо кожен елемент для точної інвалідації getUsers: builder.query({ query: () => 'users', providesTags: (result) => result ? [...result.map(({ id }) => ({ type: 'User', id })), { type: 'User', id: 'LIST' }] : [{ type: 'User', id: 'LIST' }], }) ``` Без тегів кеш залишається застарілим після мутації. Це найпоширеніша помилка в code review з RTK Query, яку я бачу знову і знову. **2. Ручний `refetch()` замість `invalidatesTags`.** ```javascript // Неправильно: оновлює тільки цей один запит const [refetch] = useLazyGetPostQuery(); refetch(); // Правильно: інвалідує всі запити, що залежать від Post addPost: builder.mutation({ query: (body) => ({ url: 'posts', method: 'POST', body }), invalidatesTags: ['Post'], }) ``` Ручний refetch ламає ланцюжок залежностей. Інші запити, що повертають ті самі дані, залишаються застарілими. **3. Побічні ефекти всередині `queryFn`.** ```javascript // Неправильно: очищає localStorage у функції, що може виконуватись паралельно queryFn: async () => { localStorage.clear(); return { data }; } ``` `baseQuery` може виконуватися паралельно для кількох ендпоінтів. Побічні ефекти тут викликають race conditions і проблеми з SSR. Тримай `queryFn` чистою функцією. **4. Не використовувати `skip`, коли аргумент ще не готовий.** ```javascript // Неправильно: відправляє запит з undefined в URL const { data } = useGetUserQuery(userId); // Правильно: чекаємо поки userId буде доступний const { data } = useGetUserQuery(userId, { skip: !userId }); ``` RTK Query використовує аргумент як ключ кешу. `undefined` як аргумент валідний, але запит відправляється з `undefined` в URL, що майже ніколи не потрібно. ### Де зустрічається в продакшені - E-commerce: список продуктів і кошик пов'язані тегами (додавання до кошика автоматично оновлює наявність товару). - Адмін-панелі: `pollingInterval: 30000` для live-лічильників замовлень без WebSockets. - React Native: кеш RTK Query зберігається через `redux-persist` для офлайн-режиму. - Монорепо: один `createApi` спільний між пакетами через `injectEndpoints`. - Auth: `prepareHeaders` вставляє JWT з Redux-store в кожен запит. ### Follow-up питання **Q:** Як покроково працює інвалідація за тегами? **A:** Мутація з `invalidatesTags: ['Post']` виконується після резолву запиту. RTK Query знаходить у кеші всі запити, що оголосили `providesTags: ['Post']`, позначає їх застарілими і рефетчить при наступному render-циклі. **Q:** У чому різниця між `query` і `queryFn`? **A:** `query` повертає URL-рядок або конфіг-об'єкт, який `baseQuery` загортає у fetch-виклик. `queryFn` дає повний контроль над запитом - корисно для GraphQL, WebSockets або будь-якого не-HTTP джерела даних. **Q:** Як відновлювати auth-токен всередині RTK Query? **A:** Загортаємо `fetchBaseQuery` в кастомний `baseQueryWithReauth`. Перевіряємо на 401, робимо спробу рефрешу токена, оновлюємо store і повторюємо оригінальний запит. Redux Toolkit docs називають цей патерн automatic re-authorization. **Q:** Що відбувається з кешем після анмонту компонента? **A:** RTK Query запускає відлік `keepUnusedDataFor` (60 секунд за замовчуванням). Коли він спливає, запис у слайсі видаляється. `keepUnusedDataFor: Infinity` закріплює дані назавжди, `0` видаляє одразу після анмонту. **Q:** (Senior) Як реалізувати infinite scroll в RTK Query без неконтрольованого зростання пам'яті? **A:** Використовуй `serializeQueryArgs` для злиття сторінок в один ключ кешу, `merge` для додавання нових сторінок і `forceRefetch` для визначення зміни сторінки. Для контролю пам'яті відслідковуй завантажені номери сторінок і витісняй старі через `api.util.updateQueryData` або інвалідацію тегів конкретних сторінок, коли вони виходять з viewport. ## Приклади ### Базовий: API для користувачів з мутацією ```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'], // рефетчить getUsers після оновлення }), }), }); export const { useGetUsersQuery, useUpdateUserMutation } = usersApi; function UserList() { const { data, isLoading } = useGetUsersQuery(); const [updateUser] = useUpdateUserMutation(); if (isLoading) return <div>Завантаження...</div>; return ( <ul> {data?.map(user => ( <li key={user.id}> {user.name} <button onClick={() => updateUser({ id: user.id, name: 'Нове ім\'я' })}> Перейменувати </button> </li> ))} </ul> ); } ``` Перойменування користувача інвалідує тег `User`, тому `getUsers` автоматично рефетчиться. Ніякого ручного dispatch. Ніякого `useEffect` для синхронізації стану. ### Продакшн-патерн: e-commerce з гранульованими тегами ```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' }], }), }), }); ``` Гранульовані теги означають, що додавання одного товару до кошика рефетчить список продуктів, але не кожну окрему сторінку товару. Це зменшує зайвий мережевий трафік у великих дашбордах. ### Просунутий: умовний запит з polling і трансформацією результату ```javascript function AdminPanel({ user }) { const { activeCount, isFetching } = useGetActiveOrdersQuery(undefined, { skip: !user?.isAdmin, // запит не йде якщо не адмін pollingInterval: user?.isAdmin ? 30000 : 0, // опитування кожні 30с selectFromResult: ({ data, isFetching }) => ({ activeCount: data?.filter(o => o.status === 'active').length ?? 0, isFetching, }), }); return ( <div> Активні замовлення: {activeCount} {isFetching && '(оновлення)'} </div> ); } ``` `skip: true` повністю скасовує підписку - це зупиняє polling і запобігає витокам пам'яті. `selectFromResult` виконується після звернення до кешу, тому компонент ре-рендериться тільки коли змінюється `activeCount`, а не при кожній polling-відповіді.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.