Освоєння основ: Ключові концепції TanStack React Query

ТанStack React Query (також відома як React Query) стала справжнім проривом для розробників, які працюють з даними в додатках на React. Коли ви створюєте сучасні веб-застосунки, які покладаються на віддалені дані, управління отриманням, кешуванням та оновленням стану може швидко стати складним. ТанStack Query абстрагує цю складність, пропонуючи безшовний, ефективний та потужний спосіб управління серверним станом у React.

У цьому пості ми розглянемо деякі основні можливості, які роблять React Query таким потужним, такі як кешування, пагінація, фонове оновлення та багато іншого. Наприкінці ви зможете краще зрозуміти, як використовувати ТанStack React Query для оптимізації управління даними у вашому додатку.

Є два основні концепти ТанStack React Query:

  • Запити (Queries)
  • Мутації (Mutations)

Давайте створимо проект, де ми будемо використовувати React Query для роботи з блог-постами. Для цього ми будемо використовувати REST API з наступними кінцевими точками:

  • /post: GET для отримання постів і POST для створення поста.
  • /notification: GET метод для отримання сповіщень.

У цьому проекті ми навчимося взаємодіяти з цими кінцевими точками за допомогою React Query в додатку на ReactJS.

Перед початком давайте налаштуємо середовище:

  • Передумови: Створіть базовий додаток за допомогою CRA (Create-React-App).
  • Встановіть react-query у ваш проект, виконавши команду npm install @tanstack/react-query або yarn add @tanstack/react-query у терміналі.
  • Щоб почати працювати з React Query, перший крок — це обгорнути весь ваш додаток в ReactQueryClientProvider. Цей провайдер дає доступ до клієнта React Query по всьому додатку, який буде використовуватися для взаємодії з кешем.
  • React Query DevTools (опційно): Він надає візуальний інтерфейс, який дозволяє вам інспектувати запити, мутації та кеш в реальному часі, допомагаючи відслідковувати стан ваших даних і швидко усувати проблеми. Щоб встановити react-query DevTools, виконайте команду npm i @tanstack/react-query-devtools або yarn add @tanstack/react-query-devtools.
    Після встановлення ви можете легко додати Devtools у ваш додаток.

Відкрийте index.js і додайте це:

import { QueryClient, QueryClientProvider } from '@tanstack/react-query';  
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';  

const queryClient = new QueryClient();  

const root = ReactDOM.createRoot(document.getElementById('root'));  
root.render(  




);

Тепер давайте детальніше зануримося в світ React Query.

1. Запити (Queries)

У світі React Query запит (Query) — це запит на отримання даних, який залежить від асинхронного джерела (зазвичай API).

Це спрощує взаємодію з сервером, надаючи потужні хуки, де хук useQuery є основним інструментом для отримання даних (GET запити).

Для початку нам потрібен простір, де буде відображатися дані про блог-пости. Створимо компонент з назвою PostList і включимо його в App.jsx:

Posts
 {/* Це наш компонент Blog Post */}    
 ```  Створіть файл з назвою `PostList.jsx`. У `PostList` ми будемо отримувати список постів за допомогою хука `useQuery` від React Query і відображати ці дані.  ``` const { data, isError, isLoading, error } = useQuery({    queryFn: fetchPosts,    queryKey: ['posts'],    }); ```  Тут `fetchPosts` — це асинхронний метод, який здійснює API запит. Ми використовували `fetch` з JavaScript для виклику `/post`, альтернатива — це **Axios**.  ``` export const fetchPosts = async () => {    const response = await fetch(    `http://example.com/post`    );    const postData = await response.json();    return postData;   }; ```  Давайте розберемо хук `useQuery`.
Він приймає два обов'язкових параметри:

- `queryKey`: Унікальний ключ для запиту, який використовується React Query для кешування даних.
- `queryFn`: Функція, яка повертає проміс, що врешті-решт поверне дані.

Хук `useQuery` повертає багато властивостей для відстеження та управління станом процесу отримання даних. Деякі з них:

- `isLoading`: Показує, чи дані ще завантажуються, що корисно для відображення індикаторів завантаження.
- `isError`: Показує, чи сталася помилка під час отримання даних (наприклад, якщо `queryFn` не вдалося виконати).
- `data`: Містить відповідь від сервера (наприклад, отримані дані), коли запит успішно завершено.

З цими властивостями ми можемо умовно відображати дані.   
Наприклад, якщо дані ще завантажуються, ми показуємо повідомлення про завантаження.   
Якщо сталася помилка під час отримання даних, відображаємо повідомлення про помилку. І, нарешті, коли дані успішно отримано, ми відображаємо відповідь (наприклад, список постів).

![pic](https://drive.javascript.org.ua/c841be6ba71_5jvQEfHS2sbmwDFadIY2kg_png)

_Список блог-постів_

Коли ви відкриваєте іконку React Query Devtools (якщо вона увімкнена), яка з'являється в нижньому правому кутку екрану (місце можна налаштувати), відкривається панель, що надає детальну інформацію про ваші запити та мутації. Ця панель показує велику кількість корисної інформації, що допомагає вам зрозуміти поточний стан операцій з отриманням даних.

Наприклад, вона показує список всіх активних запитів у вашому додатку. Кожен запит показує:

- **Query Key**: Унікальний ідентифікатор запиту.
- **Status**: Статус запиту (чи він в процесі завантаження, чи сталася помилка, чи успішно виконаний).
- **Data**: Поточні кешовані дані для запиту.
- **Error**: Якщо сталася помилка, буде показано повідомлення про помилку.
- **Last Updated**: Часова мітка останніх отриманих даних.

Також DevTools надає корисні опції для симулювання різних поведінок, з якими ви можете зіткнутися під час розробки. Ці функції допоможуть вам тестувати і налагоджувати різні сценарії, спрощуючи обробку крайніх випадків і розуміння того, як ваш додаток поводиться в певних умовах.

- **Refetch**: Ви можете вручну викликати повторне отримання запиту.
- **Invalidate**: Опція Invalidate дозволяє позначити запит як недійсний, що змушує React Query вважати дані за застарілі та викликати повторне отримання при наступному доступі до запиту.
- **Trigger Error Scenario**: Ви можете симулювати сценарій помилки, вручну викликавши помилку для запиту. Це може бути корисно, коли ви хочете протестувати, як ваш додаток обробляє невдалі запити.

![pic](https://drive.javascript.org.ua/93527550281_M32Y32N_Vnz0vYbLBYk55w_png)

_Інструменти розробника React Query_

## **Додаткові концепції**

React Query надає багато функціоналу "з коробки", все залежить від того, що вам потрібно.

1. **staleTime**

Як ви могли помітити на попередньому зображенні, дані про пости показуються як застарілі. Це тому, що за замовчуванням `useQuery` вважає кешовані дані застарілими.

Щоб змінити таку поведінку, ми можемо налаштувати параметр `staleTime` на необхідну тривалість. Цей параметр визначає, як довго дані вважаються "свіжими", перш ніж React Query позначить їх як застарілі і ініціює повторне отримання. Встановлення більшого значення для `staleTime` означає, що запити не будуть часто повторно отримувати дані і будуть використовувати кешовані дані.

Давайте встановимо `staleTime` рівним `3 хвилинам (3*60*1000 = 18000ms)` для нашого запиту на отримання постів.

const { data, isError, isLoading, error } = useQuery({
queryFn: fetchPosts,
queryKey: ['posts'],
staleTime: 18000, // за замовчуванням це 0, тому дані будуть завжди застарілими.
});
```

Протягом перших 3 хвилин дані про блог-пости вважатимуться свіжими. У цей час будь-які компоненти, що запитують дані, отримають їх безпосередньо з кешу, і жоден запит до мережі не буде виконано.

pic

Свіжі дані блог-постів

Після 3 хвилин статус даних про пости зміниться на Застаріле.
Якщо інший компонент спробує отримати доступ до даних після цього моменту, оскільки вони позначені як застарілі, буде ініційовано новий запит до мережі для отримання оновлених даних з сервера.

2. refetchInterval

У деяких випадках, коли дані потрібно показувати в реальному часі, їх потрібно часто оновлювати. Зробити це в звичайному React було б дуже складно, використовуючи setInterval.

Завдяки React Query, ми можемо налаштувати той самий хук useQuery для відправлення повторюваних запитів на сервер з фіксованими інтервалами. Для цього потрібно просто додати параметр refetchInterval з потрібною тривалістю між запитами. Це забезпечить автоматичне повторне отримання даних через задані інтервали, підтримуючи клієнтські дані в актуальному стані.

Розглянемо приклад використання в нашому проекті, де ми хочемо показувати кількість сповіщень у реальному часі.

Для цього ми створимо компонент Header, який буде відображати кількість сповіщень, як показано нижче:

const Header = () => {  
 const { data: notificationData } = useQuery({  
 queryFn: fetchNotifications,  
 queryKey: ['notifications'],  
 refetchInterval: 180000,  
 });  
 return (  

My Application
    {notificationData?.notificationUnreadCount}        
    );  
};  

У цьому компоненті ми створили запит з ключем notifications, який буде отримувати дані про сповіщення. Ми також встановили параметр refetchInterval рівним 3 хвилинам (180000 мс). Це означає, що кожні 3 хвилини React Query автоматично викликатиме API для отримання останньої кількості сповіщень, забезпечуючи регулярне оновлення даних.

Налаштувавши цей інтервал, ми гарантуємо, що кількість сповіщень завжди буде актуальною, без потреби в ручному оновленні.

Автоматичне фонове отримання даних продовжуватиметься, поки компонент, що викликає useQuery, залишатиметься змонтованим. Однак коли компонент буде демонтувано — наприклад, через умовне рендеринг або навігацію через клієнтський маршрут — відповідні спостерігачі очищаються. Тоді фонове повторне отримання припиняється, оскільки дані більше не потрібні.

3. gcTime

Кожного разу, коли ми викликаємо useQuery, створюється спостерігач, щоб відстежувати стан запиту. Поки компонент, що викликає useQuery, змонтований, запит вважається активним — його дані можуть бути в різних станах, таких як свіжі, застарілі або в процесі отримання.

Однак, коли ви переходите зі сторінки, наприклад, з сторінки постів на головну сторінку, дані про пости більше не потрібні, тому вони стають неактивними.

Давайте побачимо це на прикладі: спочатку дані про блог-пости залишаються активними, поки ми на сторінці постів, і вони стають неактивними, коли ми переходимо на головну сторінку.

pic

Перехід від активного до неактивного

Неактивні дані залишатимуться в кеші за замовчуванням протягом 5 хвилин.

Однак ми можемо налаштувати це за допомогою параметра gcTime на бажану тривалість. Після цієї тривалості неактивні дані будуть очищені в процесі збору сміття.

Якщо ви знову відвідаєте сторінку постів до закінчення часу gcTime, кешовані дані все ще будуть доступні, що дозволить використовувати кеш для вашого запиту. Однак, якщо ви повернетеся на сторінку постів після того, як збір сміття відбудеться (тобто час gcTime мине), буде ініційовано новий API-запит, оскільки кеш більше не міститиме даних, пов’язаних з queryKey 'posts'.

Ось приклад використання параметра gcTime:

const { data, isError, isLoading, error } = useQuery({  
 queryFn: fetchPosts,  
 queryKey: ['posts'],  
 gcTime: 18000, // час збору сміття  
 });  

4.
Дедуплікація кількох запитів для тих самих даних в один запит

Коли кілька запитів для одного й того ж запиту виконуються швидко один за одним, і один запит уже в процесі, лише перший запит буде насправді відправлений. Цей початковий запит створить обіцянку (promise), а наступні запити будуть ділитися тією ж обіцянкою. Це подібно до обмеження частоти запитів (throttling).

Чи застосовуються принципи дедуплікації для refetch?

За замовчуванням НІ, у випадку refetch; поточний запит буде скасовано, перш ніж буде зроблений новий запит. Однак якщо ми використовуємо параметр cancelRefetch : false, то новий запит не буде виконано, якщо вже є виконуваний запит, і буде дотримуватись того ж принципу.

5. Повторні запити (Query Retries)

const { data, isError, isLoading, error } = useQuery({  
 queryFn: fetchPosts,  
 queryKey: ['posts'],  
 });

Коли хук useQuery не вдається (тобто функція запиту викидає помилку), він автоматично повторює запит до тих пір, поки кількість спроб не досягне максимальної кількості повторів (за замовчуванням — 3).

Щоб зрозуміти це, давайте змінимо URL API на неіснуючий, наприклад, /pos. Ми можемо побачити в панелі мережі браузера, що React Query робить 4 спроби запиту: оригінальний запит плюс 3 повтори (за замовчуванням).

export const fetchPosts = async () => {  
 const response = await fetch(`http://example.com/pos`);  
 const postData = await response.json();  
 return postData;  
};

pic

Повтор запиту

Ми можемо налаштувати цю поведінку повтору відповідно до конкретних вимог:

  • retry : false вимикає повтори.
  • retry : 4 повторює невдалі запити 4 рази, перш ніж показати остаточну помилку, що виникла у функції.
  • retry = true буде повторювати нескінченно. Використовуйте це тільки в разі, якщо ви на 100% впевнені, що хочете повторювати запит безмежно, оскільки це може вплинути на продуктивність.
  • retry : (failureCount: Number, error: Error) => Boolean | Number дозволяє використовувати власну логіку на основі типу помилки і кількості спроб, що не вдалися.

Припустимо, ми знаємо, що наш сервер буде недоступний через технічне обслуговування протягом певного часу, і протягом цього періоду він буде повертати помилку 503 (service unavailable).
Щоб обробити цей сценарій, ми можемо реалізувати власну стратегію повторів, наприклад:

retry: (failureCount, error) => {  
 if (error.status === 503) {  
 return false;  
 }  
 return failureCount < 2;  
 },

Ми також можемо використовувати зворотній виклик retry для реалізації власного логування або обробки помилок, наприклад так:

retry: (failureCount, error) => {  
 console.error(  
 `Не вдалося отримати дані: ${error.message} у ${failureCount} спробах`  
 );  
 return failureCount < 3;  
 },

За замовчуванням, повтори не виконуються відразу після помилки. Замість цього застосовується затримка між спробами, яка поступово збільшується з кожною новою спробою. За замовчуванням, затримка retryDelay становить 1000 мс і подвоюється з кожною новою спробою, обмежуючи максимальну затримку до 30 секунд між спробами. Також можна переоприділити стандартну функцію/ціле значення retryDelay.

**6.
переклад

Paginated Queries (Пагіновані запити)**

Під час роботи з пагінованими даними це є дуже поширеним випадком використання, і в React Query все працює автоматично, якщо додати інформацію про сторінку в ключ запиту:

const { data, isError, isLoading, error } = useQuery({  
 queryFn: () => fetchPosts(page), // передаємо номер сторінки  
 queryKey: ['posts', page]  
 });
export const fetchPosts = async (page) => {  
 const response = await axios.get(  
 `http://example.com/post?page=${page},limit=10` // API має приймати номер сторінки  
 );  
 return response.data;  
};
    {isLoading && 
Loading...
}    {isError && 
{error?.response?.data?.message}
}    
    {data?.post?.map((post) => (    
{post.title}
    ))}    
 {    setPage(page - 1);    }}    >    Prev        {page}     {    setPage(page + 1);    }}    disabled={page <= 1}    >    Next        
 ```  ![pic](https://drive.javascript.org.ua/4b6895f9c61_BmyoWH7v6KHgS2MbBeAnmg_png); ```  Коли нові дані прибувають, попередні дані безшовно змінюються на нові.  Інша властивість `isPlaceholderData` доступна в відповіді `useQuery`, щоб визначити, чи наразі показуються дані-заповнювачі. Це можна використовувати, щоб дізнатися, чи запит ще триває, і умовно відображати стани завантаження. Таким чином, ви можете показувати заповнювачі або індикатори завантаження без блокування всього екрану, покращуючи загальний досвід користувача.  ``` 
 {    setPage(page - 1);    }}    >    Prev        {page}       {isPlaceholderData && }        {    setPage((old) => old + 1);    }}    disabled={isPlaceholderData}    >    Next        
 ```  ![pic](https://drive.javascript.org.ua/315121f2381_d_jlFGs_LoAkb_UhhjCFTg_gif) разом із `nextCursor`, який можна використовувати для отримання наступної групи сповіщень.  Наприклад, відповідь API може виглядати так:  ``` {    "notifications": [    { "id": 1, "message": "Notification 1" },    { "id": 2, "message": "Notification 2" },    // ...
переклад

],
"nextCursor": "abc123"
}
```

Для цього ми будемо використовувати ще один важливий хук, наданий React Query, під назвою useInfiniteQuery.

Ми можемо створити функціонал "Завантажити ще" за допомогою таких кроків:

  • Чекаємо, поки useInfiniteQuery запитає першу групу даних, надаючи initialPageParam
  • Логіка для повернення інформації, необхідної для наступного запиту, в функції зворотного виклику getNextPageParam
  • Виклик функції fetchNextPage, коли ми хочемо завантажити більше даних.
export const fetchNotifications = async ({ pageParam }) => {  
 const response = await axios.get(  
 `api/notification?cursor=${pageParam}`  
 );  
 return response.data;  
};
const {  
 data: notificationData,  
 fetchNextPage,  
 hasNextPage,  
 isFetchingNextPage,  
 isLoading,  
 } = useInfiniteQuery({  
 queryFn: fetchNotifications,  
 queryKey: ['notifications'],  
 initialPageParam: 0,  
 getNextPageParam: (response) => {  
 if (response.notification.length >= 5) {  
 return response?.nextCursor;  
 }  
 },  
 });

Давайте реалізуємо це в компоненті Notification,

  • Перші дані автоматично запитуються, коли компонент завантажується.
  • Коли користувач натискає кнопку Завантажити ще, викликається fetchNextPage, що завантажує наступну сторінку даних. Ця кнопка відображається лише тоді, коли є ще одна сторінка даних для завантаження (hasNextPage).
  • hasNextPage: цей стан показує, чи є ще сторінки для завантаження. Булевий параметр hasNextPage дорівнює true, якщо getNextPageParam повертає значення, яке не є null або undefined.
  • isFetchingNextPage: цей стан вказує, чи зараз завантажується наступна сторінка.
Notifications
    {notificationsData?.map((notification) => (    
    {notification.message}    
    ))}    
 fetchNextPage()}    disabled={!hasNextPage || isFetchingNextPage}    >    {isFetchingNextPage    ? 'Loading more...'    : hasNextPage    ? 'Load More'    : 'Nothing more to load'}        
{isFetchingNextPage ? 'Fetching...' : null}
 ```  ![pic](https://drive.javascript.org.ua/6353a9dcbc1_cesAKGXup5yep0OmuK7FPw_gif)  _використання useInfiniteQuery_  ## **8. select**  Параметр `select` в хуці `useQuery` дозволяє фільтрувати або перетворювати дані, що повертаються вашою функцією запиту. Це особливо корисно, коли кілька компонентів використовують однакові дані запиту, і різні компоненти потребують різних частин даних або хочуть змінити їх для зручнішого використання.  Використовуючи параметр `select`, ви можете підписатися на підмножину даних запиту, що допомагає оптимізувати продуктивність. Цей підхід мінімізує непотрібні повторні рендери та дозволяє ефективно перетворювати дані, пристосовані до потреб вашого компонента.  Припустимо, ми хочемо відобразити загальну кількість сповіщень на домашній сторінці.  ```
const Home = () => {  
 const { data: notificationData, isLoading } = useQuery({  
 queryFn: fetchNotifications,  
 queryKey: ['notifications'],  
 });  
 return (    
    {isLoading && 
Loading...
}    
Welcome to the Home Page

 you have total notifications :  
 {notificationData?.notificationCount}  

    );   
};

pic Notification Коли кнопка Завантажити ще натискається, це викликає повторний рендер заголовка з новими завантаженими сповіщеннями.
переклад

However, this also causes the entire Home page to re-render, even though the count has not been changed.

The reason for this is that both the Header and the Home page are subscribed to the notificationData state, which leads to unnecessary re-renders when the state updates, even if the Home page doesn't need to update its content.

pic

Перерендер домашньої сторінки

Щоб вирішити це, ми можемо оптимізувати рендеринг, застосувавши трансформацію даних в секції select. Таким чином, домашня сторінка буде перерендерюватися лише тоді, коли змінюється кількість сповіщень, а не коли оновлюються всі дані сповіщень.

const Home = () => {  
 const { data: notificationData, isLoading } = useQuery({  
 queryFn: fetchNotifications,  
 queryKey: ['notifications'],  
 select: (data) => {  
 return {  
 notificationCount: data?.pages[0].notificationCount,  
 };  
 },  
 });  
 return (  

    {isLoading && 
Loading...
}    
Welcome to the Home Page

 you have total notifications :  
 {notificationData?.notificationCount}  

    );   
};

pic Перерендер тільки заголовка

2. Mutations (Мутації)

Другою важливою функцією, яку надає React Query, є mutations (мутації). Вони зазвичай використовуються для створення/оновлення/видалення даних або виконання серверних побічних ефектів (методи PUT, POST і DELETE). Для цього TanStack Query надає хук useMutation.
useMutation повертає багато важливих властивостей, і одна з них — це функція mutate, яку можна викликати щоразу, коли потрібно здійснити мутацію (модифікацію даних).

Давайте додамо форму на екран зі списком постів. Ця форма буде корисна для додавання нових постів.

    {isLoading && 
Loading...
}    {isError && 
{error?.message}
}       
// Форма для додавання нового блогу  
        {isPostError ? 
An error occurred
 : null}    
    {isSuccess ? 
Post added!
 : null}            
    {isPending ? 'Posting...' : 'Post'}               
    {data?.post?.map((post) => (    
{post.title}
    ))}    
 {    setPage(page - 1);    }}    >    Prev        {page}    {isPlaceholderData && }     
 {    if (!isPlaceholderData) {    
    setPage((old) => old + 1);    
}    }}    disabled={isPlaceholderData || data?.totalPages === page}    >    Next        

Функція handleSubmit викликає функцію mutate, яка додає новий пост на сервер:

const {    
 mutate,    
 isError: isPostError,    
 error: postError,    
 isPending,    
} = useMutation({    
 mutationFn: addPost    
});       

const handleSubmit = async (e) => {    
 e.preventDefault();    
 const newPost = e.target[0].value;    
 mutate({ title: newPost });    
 e.target[0].value = '';    
};
export const addPost = async (newPost) => {    
 const response = await axios.post('http://example.com/post', {    
 title: newPost.title,    
 });    
 return response.data;   
};

pic Мутація **1.
переклад

mutateAsync (mutateAsync)

Хук useMutation надає дві функції: mutate та mutateAsync.

  • mutate не повертає нічого; він тригерить мутацію, але не надає результату.
  • mutateAsync, з іншого боку, повертає Promise, який резолвиться з результатом мутації.

В результаті, вам може бути зручніше використовувати mutateAsync, коли потрібно отримати доступ до відповіді мутації, оскільки це дозволяє обробляти результат асинхронно.

const handleSubmit = async (e) => {  
 e.preventDefault();  
 const newPost = e.target[0].value;  
 try {  
 const data = await mutateAsync({ title: newPost });  
 console.log(data);  
 } catch (err) {  
 console.log(err);  
 }  
 e.target[0].value = '';  
 };
const {  
 mutateAsync,  
 isError: isPostError,  
 isPending,  
 isSuccess,  
 variables,  
 } = useMutation({  
 mutationFn: addPost,  
 onSuccess: () => console.log('successful!!'),  
 onError: () => console.error('Something went wrong!!')  
 });

З mutate ви все ще можете отримати доступ до даних або помилок через надані зворотні виклики (onSuccess, onError), і вам не потрібно самостійно обробляти помилки. React Query обробляє помилки всередині, ловлячи та скидаючи їх, тому вам не потрібно турбуватись про невиконані відхилення обіцянок.

Зворотні виклики onError та onSuccess є самозрозумілими за їхніми іменами. Ці функції викликаються в залежності від результату мутації. Якщо сталася помилка, зворотний виклик onError надасть доступ до деталей помилки. У зворотньому випадку, якщо мутація успішна, зворотний виклик onSuccess дасть нам доступ до повернутих даних. Це дозволяє нам динамічно регулювати поведінку або стан програми в залежності від результату операції.

З іншого боку, mutateAsync дає вам контроль над повернутим Promise, що означає, що вам потрібно вручну обробляти помилки за допомогою try-catch або .catch(). Без цього ви можете зіткнутися з невиконаними відхиленнями обіцянок. Тому обробка помилок є більш прямолінійною з mutate, оскільки React Query обробляє це для вас.

У більшості випадків mutate достатньо для ефективної обробки мутацій. Однак mutateAsync є кращим, коли вам потрібен доступ до Promise, особливо у випадках, коли інші мутації залежать від поточної мутації. З mutate ви можете потрапити в "пекло зворотних викликів" (callback hell), оскільки потрібно вкладати зворотні виклики для обробки послідовності операцій, тоді як mutateAsync дозволяє чистіше управляти залежними мутаціями за допомогою async/await та ланцюжків.

2. Invalidations from Mutations (Інвалідації після мутацій)

Як показано на наведеному вище зображенні, після того як запит POST виконується, і новий контент поста успішно додається, сторінка не відразу відображає оновлений контент. Це поширений випадок, коли після операції мутації можуть бути запити, які залежать від тих самих даних у додатку, і ці запити потрібно інвалідувати або повторно запитати, щоб відобразити зміни.

Щоб вирішити це, ми можемо використати параметр onSuccess хука useMutation. Коли мутація успішно виконується, ми можемо тригерити інвалідацію будь-яких пов'язаних запитів за допомогою функції invalidateQueries клієнта React Query (який ми створили на початку проєкту). Це забезпечить, що дані будуть повторно запитуватись і новий контент поста відразу з'явиться на UI.

const {  
 mutate,  
 isError: isPostError,  
 isPending,  
 isSuccess,  
 } = useMutation({  
 mutationFn: addPost,  
 onSuccess: () => {  
 queryClient.invalidateQueries('posts');  
 },  
 });

Це дозволить нам забезпечити, щоб останній контент поста миттєво з'явився на екрані після виконання мутації.

pic

Мутація з інвалідацією запиту

3. onMutate (onMutate)

React Query надає можливість оптимістично оновлювати UI до завершення мутації. Ви можете використовувати параметр onMutate для прямого оновлення вашого кешу. Цей необов'язковий зворотний виклик дозволяє виконувати логіку до того, як буде викликана функція мутації.
переклад

It receives the same variables that the mutation function would normally get.

Putting data into the cache directly via setQueryData will act as if this data was returned from the backend, which means that all components using that query will re-render accordingly.

const {  
 mutate,  
 isError: isPostError,  
 isPending,  
 isSuccess,  
 } = useMutation({  
 mutationFn: addPost,  
 onMutate: (newPost) => {  
 // Cancel any outgoing refetches  
 // (so they don't overwrite our optimistic update)  
 queryClient.cancelQueries(['posts', page]);  
 const previousData = queryClient.getQueryData(['posts', page]);  
 queryClient.setQueryData(['posts', page], (old) => {  
 return {  
 ...old,  
 post: [...old.post, { id: 8, title: newPost.title }],  
 };  
 });  
 return { previousData };  
 },  
 onSuccess: () => {  
 queryClient.invalidateQueries(['posts', page]);  
 },  
 });

pic

Оптимістичне оновлення з onMutate

На зображенні вище ми бачимо, що запит POST все ще знаходиться в стані очікування. Однак інтерфейс був оптимістично оновлений з новими даними, очікуючи, що мутація успішно завершиться. Це стало можливим, оскільки ми безпосередньо оновили кеш, дозволяючи інтерфейсу відобразити очікувані зміни, поки мутація не завершиться.

Коли ви оптимістично оновлюєте стан до виконання мутації, завжди є ймовірність, що мутація може не вдатися. У багатьох випадках просте повторне запитування ваших оптимістичних запитів може відновити стан так, щоб він відповідав справжньому стану сервера. Однак в деяких сценаріях повторний запит може не працювати, як очікується, особливо якщо невдача мутації спричинена проблемою на сервері, яка заважає успішному повторному запиту. У таких випадках ви можете вирішити скасувати своє оптимістичне оновлення.

Значення, яке повертається з зворотного виклику onMutate, передається як в зворотний виклик onError, так і в onSettled. Цей механізм особливо корисний для скасування оптимістичних оновлень, щоб забезпечити правильне відновлення стану, якщо виникає помилка під час мутації.

onError: (error, newPost, context) => {  
 queryClient.setQueryData(['posts', page], context.previousData);  
 }

4. Оптимістичні оновлення за допомогою змінних

Це простіший підхід до обробки оптимістичних оновлень, оскільки він не вимагає безпосередньої взаємодії з кешем. Ми будемо мати доступ до змінних мутації, які містять нові дані поста. В компоненті, де рендериться запит, ми можемо тимчасово додати елемент до списку, поки мутація ще в процесі.

З допомогою змінних ми можемо оновлювати лише поточний компонент, це не впливає на інші компоненти, які підписані на той самий ключ запиту, як це відбувається з onMutate.

const {  
 mutate,  
 isError: isPostError,  
 isPending,  
 isSuccess,  
 variables, // <-- Це
 } = useMutation({  
 mutationFn: addPost,  
 onSuccess: () => {  
 queryClient.invalidateQueries(['posts', page]);  
 }  
 });
    {data?.post?.map((post) => (    
{post.title}
    ))}    {isPending && 
{variables.title}
}    
 ```  Під час цього часу елемент відображається з зменшеним рівнем непрозорості, щоб показати, що це тимчасове доповнення. Як тільки мутація завершується, якщо вона успішна, елемент автоматично відображається як звичайний елемент у списку.  

![pic](https://drive.javascript.org.ua/4414be019c1_w0UKBYOGrKKztH6KD6PpsQ_png)  _Оптимістичне оновлення з змінними_

Якщо мутація не вдається, елемент зникне, що гарантує, що інтерфейс залишиться узгодженим з фактичним станом сервера.  

![pic](https://drive.javascript.org.ua/5ba75abd541_OxwB2BXUbq4hRm8NgqQxDA_gif)  _Оптимістичне оновлення, яке призводить до помилки_

> Не кожну мутацію потрібно виконувати оптимістично. Ви повинні бути впевнені, що вона рідко зазнає невдач, оскільки користувацький досвід при скасуванні не найкращий.  **5.
переклад

onSettled (onSettled)

Коли потрібно виконати операції незалежно від результату мутації, можна використовувати цей зворотний виклик. Він дозволяє викликати функцію, коли мутація або успішна, або виникає помилка, і отримати або дані, або помилку.

// Завжди повторно запитувати після помилки або успіху:
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['posts', page] })
},
```

Також можна використовувати функцію onSettled замість окремих обробників onError та onSuccess, якщо бажаєте:

onSettled: (newPost, error, variables, context) => {  
 if (error) {  
 // зробіть щось  
 }  
 },

6. retry (повторна спроба)

За замовчуванням React Query не буде повторювати мутацію при помилці, але це можна налаштувати за допомогою опції retry:

const {  
 mutate,  
 isError: isPostError,  
 isPending,  
 isSuccess,  
 variables,  
 } = useMutation({  
 mutationFn: addPost,  
 onSuccess: (response) => {  
 console.log('post data call', response);  
 queryClient.invalidateQueries(['posts', page]);  
 },  
 onError: (error, newPost, context) => {  
 console.log('error', error);  
 },  
 retry: 3,  
 });

Коли мутація зазнає невдачі (функція запиту кидає помилку), вона повторюватиме мутацію. Ми можемо налаштувати автоматичне повторення мутації або запустити повторну спробу на основі певних помилок, або вимкнути повтори після певної кількості спроб або failureCount, подібно до хука useQuery.

retry: (failureCount, error) => {  
 if (error.response?.status === 404) {  
 // Не повторювати, якщо статус помилки — 404  
 return false;  
 }  
 // Повторити до 3 разів для інших помилок  
 return failureCount < 3;  
 },

7. Оновлення з відповідей мутацій

Коли працюєте з мутаціями, які оновлюють об'єкти на сервері, часто сервер повертає оновлений об'єкт у відповіді на мутацію. Замість того, щоб повторно запитувати відповідний запит і виконувати непотрібний мережевий запит для даних, які ми вже маємо, ми можемо скористатися об'єктом, повернутим мутацією. Використовуючи метод setQueryData клієнта запитів, ми можемо негайно оновити існуючий запит новими даними, гарантуючи, що інтерфейс залишатиметься в синхронізації без додаткових мережевих запитів.

onSuccess: (response) => {  
 console.log('post data call', response);  
 queryClient.setQueryData(['posts', page], (oldData) =>  
 oldData  
 ? {  
 ...oldData,  
 post: [...oldData.post, response],  
 }  
 : oldData  
 );  
 }

React Query — Роздуми та компроміси

1. Розмір бандла

Оцінюючи бібліотеку, розмір бандла є важливим аспектом, особливо перед додаванням нової залежності до вашого проєкту. Якщо імпортувати все з React Query, ми отримаємо 15.6 kB gzip (продукційний бандл). Це не мало, але й не дуже багато.

pic

Але це лише коли ви використовуєте всі функції бібліотеки, тому це не є типовою точкою старту. Зазвичай ви можете значно досягти результату з простою конфігурацією, що включає лише QueryClient, QueryClientProvider, useQuery та useMutation. Це зменшує бандл до 13.1 kB.

pic

Бібліотека, як react-query, "окупає себе", оскільки чим більше ви її використовуєте, тим більше вона заощаджує вам коду, який ви б інакше мусили написати самостійно.

Тому при перевірці розміру бандла бібліотеки важливо думати не тільки про її безпосередній розмір, а й про те, що вона може заощадити в довгостроковій перспективі.

2. Значна крива навчання

React Query пропонує багатий набір функцій, який на початку може здатися перевантаженням для розробників. Однак важливо усвідомити, що вам не потрібно освоювати все одразу. Ключовим є почати з основної функції, яка дає приблизно 80% значення з самого початку.

Коли ви відчуєте комфорт, можна поступово вивчати інші функції за потребою. Такий підхід дозволяє створити міцну основу без того, щоб відчувати перевантаження від складності.
переклад

Only Manages Server State (Тільки керує станом сервера)

React Query спроектовано спеціально для керування станом сервера, а не станом клієнта. Тому вам все одно знадобиться інструмент для керування станом на стороні клієнта, такий як MobX або Redux, разом з React Query. Розрізнення між станом клієнта та станом сервера є свідомим, оскільки вони мають різні вимоги та поведінку.

Висновок

Сподіваюся, це допомогло вам зрозуміти React Query та те, як він вирішує проблеми управління станом сервера. Хоча я охопив основні концепції, є ще багато чого для вивчення в межах цієї бібліотеки. React Query пропонує безліч функцій і гнучкості, які можуть значно покращити вашу стратегію отримання даних та загальну продуктивність додатку.

Перекладено з: Mastering the Basics: Key Concepts of TanStack React Query