Головна / Статті / Детальний посібник TanStack Query: запити, кеш, зміни та оптимістичний інтерфейс користувача.

Детальний посібник TanStack Query: запити, кеш, зміни та оптимістичний інтерфейс користувача.

TanStack Query керує станом сервера, як менеджер ресторану: спільні ключі кешу, налаштування свіжості даних, скоординовані зміни, анулювання даних та оптимістичні оновлення через ваш HTTP-клієнт.

5069 слів

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

Що таке TanStack Query?

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

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

Axios / Fetch проти TanStack Query

Початківці часто думають, що TanStack Query замінює Axios або fetch. Це не так.

  • Fetch та Axios — це офіціанти. Вони передають запит на кухню та приносять відповідь. Вони не пам’ятають попередніх запитів, не оцінюють свіжість даних та не координують дії інших офіціантів.
  • TanStack Query — це менеджер. Він наймає офіціантів для виконання завдань. Він пам’ятає, що було отримано, чи вважається це ще свіжим, які столи використовують одну й ту саму зошитку, та коли потрібно надіслати когось назад після того, як кухня змінила страву.

Ви все ще пишете тіла функцій queryFn, які використовують Axios або fetch. TanStack Query обгортає ці виклики за допомогою ключів кешу, механізмів усунення дублікатів, повторних спроб та хуків життєвого циклу.

Що таке TanStack Query? (карта можливостей)

У повсякденній роботі з React найважливішими є шість можливостей:

1. Запити (useQuery): отримання даних

Декларативні операції зчитування, які керуються ключем queryKey та виконуються за допомогою queryFn.

2. Кешування: „мозок“ менеджера

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

3. Свіжість: staleTime проти gcTime

staleTime визначає, коли дані в кеші вважаються настільки застарілими, що їх потрібно оновити. gcTime (збір сміття) визначає, як довго залишатимуться невикористані записи кешу перед їх видаленням.

4. Мутації (useMutation): зміна даних

Операції запису — створення, оновлення, видалення — виконуються за потребою, а не під час ініціалізації.

5. Анулювання запитів: скасування меню

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

6. Оптимістичні оновлення: досвід, схожий на зірку Мішлен

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

У решті цього посібника кожна з цих функцій розглядається на прикладах з ресторанною сферою та конкретними фрагментами коду.

1. Запити (useQuery): отримання меню

Запит визначає, що ви хочете отримати та як це зробити:

import { useQuery } from '@tanstack/react-query';
import axios from 'axios';

// The waiter function (Axios)
const fetchMenu = async () => {
  const response = await axios.get('/api/menu');
  return response.data;
};

function MenuComponent() {
  // The Manager (TanStack Query) orchestrating the process
  const { data: menu, isLoading, isError, error } = useQuery({
    queryKey: ['menu'], // The label for this specific data
    queryFn: fetchMenu,  // The waiter doing the fetching
  });

  if (isLoading) return <div>Waiter is walking to the kitchen... Loading Menu...</div>;
  if (isError) return <div>The kitchen is on fire! Error: {error.message}</div>;

  return (
    <ul>
      {menu.map((item) => (
        <li key={item.id}>{item.name} - ${item.price}</li>
      ))}
    </ul>
  );
}

queryKey — це назва файлу в блокноті менеджера: ['menu'], ['soup'], ['allergies', tableId]. Однакові ключі використовують спільну кеш-пам’ять та усувають дублікати під час обробки запитів. queryFn — це функція, яка повертає обіцянку з даними.

Поки виконується перший запит, прапорці isPending / loading дозволяють інтерфейсу відображати тимчасові значення. Помилки виявляються через isError та error. Успішні дані з’являються у полі data та залишаються доступними для кожного компонента, який стежить за цим ключем.

Що таке умова змагання?

Аналогія з рестораном

Уявіть двох клієнтів, які прослять суп, поки кухня працює повільно. Запит столика A, який надійшов пізніше, не повинен перезаписувати новіший запит столика B. Без координації перемагає той запит, який завершиться останнім — навіть якщо його дані вже застарілі.

Як це відбувається у React (useEffect)

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

Як TanStack Query вирішує цю проблему

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

2. Кешування: блокнот менеджера

// Waiter function
const fetchMenu = async () => {
  console.log("Waiter is walking to the kitchen!"); // We can track how many times this runs
  const response = await axios.get('/api/menu');
  return response.data;
};

// Component 1: The Sidebar
function MenuSidebar() {
  const { data } = useQuery({ queryKey: ['menu'], queryFn: fetchMenu });
  return <div>We have {data?.length} items today!</div>;
}

// Component 2: The Main Display
function MenuMainDisplay() {
  const { data } = useQuery({ queryKey: ['menu'], queryFn: fetchMenu });
  return <div>{data?.map(item => <p>{item.name}</p>)}</div>;
}

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

Менеджер у дії (крок за кроком)

  1. Компонент A завантажується → не виявлено у кеші → звернення до мережі.
  2. Надходить відповідь → запис у кеш → компонент A відображається.
  3. Компонент B завантажується з тією самою ключовою інформацією → знахідка у кеші → компонент B відображається негайно.
  4. Згідно з правилами свіжості, пізніше може відбутися фонове оновлення даних без блокування першого відображення компонента B.

Стан сервера має зберігатися у TanStack Query; справжній стан користувацького інтерфейсу (відкритий модал, обраний таб) може знаходитися у стані React або у простому клієнтському сховищі.

3. Свіжість: налаштування staleTime та gcTime

1. staleTime: чи залишається ця інформація актуальною?

const { data } = useQuery({
  queryKey: ['soup'],
  queryFn: fetchSoup,
  staleTime: 1000 * 60 * 30, // 30 minutes
});

З параметром staleTime: 10_000 дані, які є свіжими протягом десяти секунд, використовуються знову після перезавантаження без повторного завантаження. Після того, як вони стають застарілими, спостерігачі можуть ініціювати фонове повторне завантаження (під час перезавантаження, отримання фокусу вікна або підключення знову — залежно від параметрів за замовчуванням). Значення staleTime слід вибирати з урахуванням частоти змін даних: списки страв на день можуть бути короткими, а списки країн — довгими.

2. gcTime: чи можу я викинути цей документ?

const { data } = useQuery({
  queryKey: ['allergies', 'table4'],
  queryFn: fetchAllergies,
  gcTime: 1000 * 60 * 60 * 24, // Keep in memory for 24 hours
});

gcTime визначає, як довго запис у кеші залишається після того, як усі спостерігачі припинили його використання. Короткий час gcTime дозволяє швидше звільнити пам’ять; довший час робить повторні відвідування сторінок миттєвими. Не плутайте його з staleTime: застарілі дані все ще можуть знаходитися в пам’яті до моменту їх очищення.

Найкраща таємниця: stale-while-revalidate

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

4. Мутації (useMutation): додавання нової страви

Що таке мутація?

Мутація змінює стан сервера — оформлення замовлення, редагування профілю, видалення коментаря.

Аналогія з рестораном: оформлення замовлення

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

Код: створення форми замовлення

import { useMutation } from '@tanstack/react-query';
import axios from 'axios';
import { useState } from 'react';

// 1. The Waiter Function (The actual network request)
const placeOrder = async (orderData) => {
  // We are using POST because we are creating a new order
  const response = await axios.post('/api/orders', orderData);
  return response.data;
};

function OrderForm() {
  const [dish, setDish] = useState('');

  // 2. The Manager orchestrating the mutation
  const mutation = useMutation({
    mutationFn: placeOrder,
    // We can also trigger side effects right here!
    onSuccess: (data) => {
      console.log("Chef says: Order confirmed!", data);
    },
    onError: (error) => {
      console.log("Chef says: We have a problem.", error.message);
    }
  });

  const handleSubmit = (e) => {
    e.preventDefault();
    // 3. Triggering the mutation and passing the variables
    mutation.mutate({ dishName: dish, tableNumber: 4 });
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        value={dish}
        onChange={(e) => setDish(e.target.value)}
        placeholder="What would you like?"
      />

      {/* Notice how we use isPending to disable the button so they don't double-order! */}
      <button type="submit" disabled={mutation.isPending}>
        {mutation.isPending ? 'Sending to Kitchen...' : 'Place Order'}
      </button>

      {/* Handling the feedback */}
      {mutation.isError && <p style={{ color: 'red' }}>Failed: {mutation.error.message}</p>}
      {mutation.isSuccess && <p style={{ color: 'green' }}>Order placed successfully!</p>}
    </form>
  );
}

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

Деталі для досвідчених розробників

1. Воно не виконується автоматично

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

2. isPending проти isLoading

У версії v5 краще використовувати isPending для позначення статусу виконання мутації. Узгодьте блокування інтерфейсу з цим флагом.

3. Запобігання проблемам від подвійного клацання

Блокуйте кнопку надсилання, поки значення isPending дорівнює true, щоб користувачі не могли створювати дублікати замовлень.

5. Скасування запиту: повідомлення менеджеру про необхідність оновлення блокнота

Проблема: застарілий блокнот

Після того, як шеф додає страву, столи, які все ще використовують кешовані меню, бачать список учорашнього дня, поки щось не оновить його.

Рішення: анулювання запитів

import { useMutation, useQueryClient } from '@tanstack/react-query';
import axios from 'axios';

function AddDishForm() {
  // 1. Get access to the Manager's office
  const queryClient = useQueryClient();

  const mutation = useMutation({
    mutationFn: async (newDish) => {
      const response = await axios.post('/api/menu', newDish);
      return response.data;
    },
    // 2. The magic happens HERE in the onSuccess callback
    onSuccess: () => {
      // 3. Tell the Manager to rip up the menu notepad
      queryClient.invalidateQueries({ queryKey: ['menu'] });
      console.log("Menu invalidated! The Manager is getting a fresh copy.");
    },
  });

  // ... form code
}

invalidateQueries позначає відповідні записи як застарілі та ініціює їх повторне завантаження для активних спостерігачів.

Що саме відбувається, коли викликається invalidateQueries?

Відповідні запити стають застарілими; спостерігачі, які підключені, завантажують дані знову; записи, які не підключені, чекають до наступного підключення (залежно від gcTime). Кухня залишається джерелом істини; записник отримує команду на оновлення.

Складна концепція: розмите збігання

Часткове анулювання:

queryClient.invalidateQueries({ queryKey: ['menu', 'lunch'] });

Або анулювати цілий префікс:

// This rips up the breakfast, lunch, and dinner notepads all at once!
queryClient.invalidateQueries({ queryKey: ['menu'] });

Метод нечіткого збігу префіксів руйнує блокноти нотаток для сніданку, обіду та вечері, коли ви широко скасовуєте значення ['menu'] — це потужно, але й небезпечно. Краще використовувати найвужчий критерій, який збереже коректність інтерфейсу.

Золоте правило мутацій

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

6. Оптимістичні оновлення: досвід зі зіркою Мішлен

Аналогія: довіра менеджера

Довірений менеджер може записати нове пиво у рахунок ще до підтвердження кухнею — а потім стерти цей запис, якщо пива немає.

Три основи оптимістичного оновлення

Усередині функції useMutation:

  1. onMutate: зупинити конфліктні запити, зробити копію кешу та негайно записати оптимістичні дані.
  • onError: відновити знімок, якщо сервер відхиляє запит.
  • onSettled: скасувати дію (або іншим чином синхронізувати) дані, щоб кеш відповідав базі даних незалежно від того, чи успішна була операція, чи ні.
  • Код: оптимістичне додавання страви

    import { useMutation, useQueryClient } from '@tanstack/react-query';
    import axios from 'axios';
    
    function AddDishForm() {
      const queryClient = useQueryClient();
    
      const mutation = useMutation({
        mutationFn: async (newDish) => {
          const response = await axios.post('/api/menu', newDish);
          return response.data;
        },
    
        // 1. The millisecond the user clicks submit...
        onMutate: async (newDish) => {
          // A. Cancel any outgoing refetches so they don't overwrite our optimistic update
          await queryClient.cancelQueries({ queryKey: ['menu'] });
    
          // B. Take a snapshot of the current menu (The Eraser Backup)
          const previousMenu = queryClient.getQueryData(['menu']);
    
          // C. Optimistically update the Manager's notepad right now!
          queryClient.setQueryData(['menu'], (oldMenu = []) => {
            // We fake an ID for now, the real ID comes from the database later
            return [...oldMenu, { ...newDish, id: Math.random().toString() }];
          });
    
          // D. Return the snapshot so onError can use it if things go wrong
          return { previousMenu };
        },
    
        // 2. If the Kitchen catches on fire...
        onError: (err, newDish, context) => {
          // Use the eraser! Roll back to the snapshot we saved in onMutate
          if (context?.previousMenu) {
            queryClient.setQueryData(['menu'], context.previousMenu);
          }
          console.error("Chef says no! Rolling back.", err);
        },
    
        // 3. Always run this at the very end, success or fail...
        onSettled: () => {
          // Tell the Manager to get the real, final menu from the database
          queryClient.invalidateQueries({ queryKey: ['menu'] });
        },
      });
    
      // ... form code
    }
    

    Зверніть увагу на скасування запитів, які ще виконуються, структуру знімків та шляхи відкату. Оптимістичний інтерфейс здається миттєвим, але ніколи не повинен залишати кеш у недійсному стані, коли кухня відмовляє.

    Об’єднання ключових елементів

    • TanStack Query — це асинхронний менеджер стану, а не засіб для отримання даних. Він обгортає Axios/fetch, щоб зробити мережеві процеси прогнозованими.
    • Ключ запиту — це все. Він визначає процеси усунення дублікатів, кешування та обміну даними між компонентами.
  • Синхронізація кешу після змін — це ваша робота. Використовуйте invalidateQueries або ретельно спроєктовані оптимістичні оновлення, щоб блок нотаток клієнта відповідав даним на сервері.
  • Практичні стандарти для реальних додатків

    Почніть із розумного значення staleTime для переважно статичних ресурсів (у хвилинах) та короткого або нульового значення staleTime для даних, які змінюються у кожного користувача. Зберігайте queryFn простим та можливим до призупинення. Централізуйте ключі у функціях-фабриках (menuKeys.list(), menuKeys.detail(id)), щоб процес анулювання залишався типізованим та послідовним. У режимі розробки фіксуйте події кешу під час діагностики дублювання запитів. Віддавайте перевагу анулюванню кешу перед складними вручну реалізованими механізмами, поки екран справді не потребує оптимістичних функцій.

    Поширені причини збоїв

    • Використання нестабільних ключів (створення нових об’єктів при кожному оновленні) руйнує кеш.
    • Забування про анулювання даних після змін призводить до відображення «привидових» даних.
    • Встановлення нескінченного значення staleTime без стратегії обробки змін заморожує інтерфейс користувача.
    • Зберігання всіх флагів клієнтського інтерфейсу в кеші запитів ускладнює розуміння меж стану сервера.
    • Надмірно широке анулювання даних (помилки у форматі queryKey: ['']) змушує систему перезавантажувати всі дані.

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

    Закінчення

    TanStack Query займає своє місце завдяки контролю над життєвим циклом даних на сервері — читанням, свіжістю інформації, записом та синхронізацією — залишаючи при цьому транспортування даних на Axios або fetch. Дізнайтеся про параметри налаштувань (ключі), регулятори свіжості (staleTime, gcTime) та процедури запису даних (мутації, анулювання, оптимістичні оновлення). Завдяки цьому додатки на React більше не змушені створювати власні кеші запитів у кожному useEffect, а починають функціонувати як добре організована їдальня.

    Чому метафора ресторану продовжує бути ефективною

    Мережеві «водоспади» здаються абстрактними, поки ви не уявите собі п’ятьох офіціантів, які біжать за одним і тим самим супом. Процес видалення дублікатів — це коли менеджер піднімає руку: один прохід, але багато столиків обслуговано. Механізм «старе поки перевіряється» — це подача останнього надрукованого меню, поки кур’єр перевіряє дошку оголошень. Анулювання даних — це виривання сторінок, коли шеф змінює рецепти. Оптимістичні оновлення — це запис замовлення гостя на рахунок ще до підтвердження, з гумкою поруч. Під час навчання молодших співробітників необхідно розглянути ці сценарії перед тим, як показувати генерики TypeScript; так краще запам’ятовується.

    Інтеграція з маршрутизаторами та системами автентифікації

    Якщо дані не є глобальними, ключі мають включати ідентифікатор орендаря чи користувача: ['menu', restaurantId] або ['allergies', userId]. Під час виходу з системи очистіть кеш, щоб уникнути витоку сторінок блокнота між користувачами. За допомогою React Router чи подібних інструментів активуйте скасування запитів після дій, які вже визначають, які ресурси змінилися, замість того щоб перезавантажувати все при кожній навігації.

    Стратегії тестування

    Тестируйте мапери queryFn окремо як модульні тести. У тестах компонентів обгортайте їх за допомогою QueryClientProvider з новим клієнтом та параметром retry: false для забезпечення детермінізму. Перевіряйте, що мутації викликають invalidateQueries з потрібними ключами. Для оптимістичних сценаріїв імітуйте помилки сервера та переконуйтеся у відкаті до попереднього стану. Уникайте спільного використання одного QueryClient у непов’язаних тестах без його скидання.

    Примітки щодо продуктивності

    Великі списки краще розміщувати за допомогою сторінкування чи безкінечних запитів, а не за допомогою одного великого ключа. Селектори (select) дозволяють компонентам спостерігати окремі частини даних, не переробляючи їх при зміні непов’язаних полей кешу. Забезпечуйте, щоб результати queryFn були серіалізованими та стабільними. Вимірюйте кількість повторних запитів, коли refetchOnWindowFocus поєднується з дуже коротким значенням staleTime у завантажених панелях керування — налаштовуйте параметри окремо для кожного запиту, а не глобально.

    Підхід до міграції від примітивного useEffect

    Замініть ефекти монтування, які встановлюють трикутники з даними про завантаження, помилки та дані, на useQuery. Замініть імперативні обробники POST-запитів на useMutation. Видаліть саморобні кеші. Зберігайте екземпляри Axios для інтерцепторів та заголовків автентифікації; передавайте їх у queryFn. Міграція відбувається поступово: робота з одним екраном за раз все ще допомагає швидше уникнути помилок конкуренції.

    Кінцевий перелік перевірок перед випуском функції

    1. Стабільний, ієрархічний queryKey.
    2. Для домену обраний явний staleTime.
    3. Мутація поєднується з анулюванням даних або підходом оптимізму.
    4. У режимі очікування забороняється подання дублікатів запитів.
    5. Налаштовані сповіщення про помилки та межі.
    6. Ключі, пов’язані з автентифікацією, очищуються після закінчення сесії.

    Якщо дотримуватися цих шести принципів, TanStack Query перестає бути „ще однією бібліотекою“ та стає ідеальним менеджером, який потрібен вашій системі.

    Покрокове пояснення: завантаження меню у трьох компонентах

    Уявіть собі заголовок, який показує суп дня, бічну панель з переліком спеціальних пропозицій на обід та основну панель, яка відображає повний меню. Без TanStack Query кожна з цих панелей могла б створювати власний useEffect та звертатися до /api/menu. Завдяки спільному значенню queryKey: ['menu', restaurantId] перший запуск виконує запит до мережі; інші панелі читають дані з кешу. Коли шеф-кухар змінює склад супу через форму адміністратора за допомогою useMutation, це скасовує значення ['menu', restaurantId], що змушує оновити всі панелі, які все ще відображаються на екрані. Гості ніколи не побачать трьох різних супів через розбіжності між офіціантами.

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

    Проектування ключів запитів як шляхів до файлів

    Розглядайте ключі як ієрархічні шляхи:

    • ['menu', restaurantId]
    • ['menu', restaurantId, 'lunch']
    • ['menu', restaurantId, 'item', itemId]
    • ['allergies', restaurantId, tableId]

    Фабрики допомагають:

    Блокування ['menu', restaurantId] може дозволити нечітке збігання глибших ключів, якщо це налаштовано, що дозволяє зберігати списки оновлень та детальні перегляди разом. Уникайте вбудовування несеріалізованих значень (функцій, екземплярів класів) всередину ключів. Віддавайте перевагу примітивним ідентифікаторам та стабільним енумерациям.

    Вибір значення staleTime з урахуванням мови продукту

    Запитайте власників продуктів, наскільки помилковим може бути інтерфейс протягом N секунд. Маркетинговий текст, який змінюється щомісяця, може дозволяти собі тривалий термін актуальності. Під час швидких розпродажів підрахунок запасів може вимагати майже нульового значення staleTime разом із скасуванням даних після кожної зміни покупки. Зазначте цей вибір поруч із запитом, щоб майбутні редагувальники не „оптимізували“ нестабільний запит, перетворюючи його на довгий термін застарілості.

    gcTime — це параметр керування пам’яттю. Мобільні додатки з великою кількістю шляхів отримують перевагу від тимчасового зберігання останніх екранів, щоб навігація назад здавалася миттєвою. Дуже великі кеш-буфери на пристроях з обмеженою пам’яттю вимагають коротшого значення gcTime або використання сторінкування.

    Зміни, які здаються безпечними

    Завжди відображайте стани очікування та помилок. Вимкніть кнопки, які можуть спричинити пошкодження даних, поки є стан очікування. У випадку видалення оптимістичне видалення має відновлювати знімок стану, якщо сервер повертає коди 409 або 500. При створенні записів оптимістичні записи потребують тимчасових ідентифікаторів клієнта, які після успішної операції мають бути замінені на ідентифікатори сервера; або ж, якщо мапування ідентифікаторів є складним, краще уникати оптимізму та просто скасувати операцію.

    Паралельні зміни для одного й того ж ключа можуть взаємно перешкоджати одна одній; розташуйте їх у черзі або вимкніть відповідні елементи керування. Функція mutateAsync у бібліотеках форм має знаходитися всередині обробників надсилання даних із блоками try/catch, а не під час відображення форми.

    Шаблони скасування, які підтримують масштабування

    Після входу скасуйте дію ключів, прив’язаних до користувача, замість того, щоб очищувати весь клієнт, якщо потрібно зберегти публічний контент. Після виходу з системи зазвичай доцільно використовувати queryClient.clear(). Коли WebSocket повідомляє про зміну меню, використовуйте ті самі функції скасування, що й при HTTP-змінах, щоб обидва шляхи використовували одну стратегію синхронізації.

    Для ймовірних сторінок деталей виконуйте попереднє завантаження при наведенні курсору: queryClient.prefetchQuery({ queryKey, queryFn }) перетворює відчутну затримку на успішні запити до кешу без змін у коді екрана.

    Оптимістичні оновлення без міфології

    Оптимізм не є обов’язковим для кожного запиту типу POST. Використовуйте його, коли успішний сценарій є поширеним, користь для інтерфейсу очевидною та можливість скасування легко реалізовується. Уникайте його, коли серверна валідація є складною або коли для відображення необхідний тіло відповіді (ідентифікатори, ціни, податки, створені на сервері). Повільне обертання індикатора може бути кращим, ніж миттєве відображення неправильних даних.

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

    Як це порівнюється з глобальними клієнтськими сховищами

    Redux чи Zustand можуть зберігати дані сервера, але вам доведеться створювати кеш заново, запитувати про усунення дублікатів та оновлювати дані у фоновому режимі. TanStack Query спеціалізується саме на цьому. Тимчасовий інтерфейс слід зберігати у локальному стані чи невеликому клієнтському сховищі; серверські елементи — у кеші запитів. Змішування цих джерел призводить до подвоєння джерел істини.

    Навчання команди

    Проведіть семінар: створіть невеликий додаток з меню, який використовуватиме запити для списку та деталей, функції створення змін, скасування даних та оптимістичне створення. Вимагайте наявності функцій для генерації ключів та очищення даних після вихіду з облікового запису. Як тільки ця схема стане автоматичною, у більших додатках припинять з’являтися помилки, пов’язані з використанням useEffect для отримання даних.

    Таблиця підсумків у прозі

    Запити — для читання. Мутації — для запису. Ключі називають рядки кешу. staleTime відповідає на запитання «Чи можу я використати ще раз, не запитуючи кухню?» gcTime відповідає на запитання «Чи можу я викинути цю сторінку блокнота?» Функція інвалідації відповідає «Кухня змінилася — оновіть інформацію». Оптимізм відповідає «Оновіть рахунок прямо зараз, видаліть його, якщо буде відхилено». Для передачі даних використовується Axios або fetch. Саме цей розподіл обов’язків і є всім продуктом.

    Сценарій від початку до кінця: табло спеціальних пропозицій на обід

    Ресторан розпочинає обідню зміну. Для запиту до табло спеціальних пропозицій використовується queryKey: ['menu', restaurantId, 'lunch'] із параметром staleTime у дві хвилини, оскільки табло на дошці мало змінюються протягом зміни. Віджет супу у заголовку використовує ['menu', restaurantId, 'soup'] із терміном дії даних у тридцять секунд. Обидва queryFn викликають один і той самий екземпляр Axios із проміжниками автентифікації. Коли адміністратор зберігає новий суп за допомогою useMutation, функція onSuccess анулює обидві ключові пари — або анулює спільний префікс ['menu', restaurantId], якщо наміром є робота з нечіткими збігами. Гості на кожному активному планшеті бачать оновлення без необхідності ручного оновлення.

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

    Фабрики ключів запитів у TypeScript

    Централізація ключів:

    • menuKeys.all(restaurantId)
    • menuKeys.lunch(restaurantId)
    • menuKeys.item(restaurantId, itemId)

    Фабрики запобігають опечаткам та дозволяють шукати недійсні дані в кодовій базі. Віддавайте перевагу туплам примітивних значень. Якщо існують фільтри, включайте серіалізовані об’єкти фільтрів із стабільним порядком ключів. Ніколи не включайте усю структуру опцій з props до ключа, якщо вона не є мемоїзованою та серіалізованою.

    Опції useQuery, з якими ви будете працювати

    Окрім queryKey та queryFn: параметр enabled блокує отримання даних доти, доки не з’являться ідентифікатори; retry керує політикою реагування на тимчасові помилки; refetchOnWindowFocus можна вимкнути для ресурсомістких панелей керування; placeholderData чи initialData забезпечують стабільність форматування; select обмежує кількість отримуваних даних для зменшення кількості переробок. Початкові значення підходять; їх потрібно налаштовувати окремо для кожного запиту, якщо профілі показують часті перезапити.

    Розуміння механізму stale-while-revalidate з точки зору користувацького інтерфейсу

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

    Мутації: анатомія надсилання форми у твердій формі

    Вимкніть кнопку для завершених операцій; відображайте помилки безпосередньо у тексті з error; при успіху скасуйте чи оновіть кеш; у разі завершення операції, за потреби, очистіть стан локальної форми. Використовуйте mutateAsync з конструкцією try/catch у обробнику надсилання форми бібліотеки. Не викликайте mutate у циклах без контролю конкурентності. Для завантаження даних відображайте прогрес окремо — TanStack Query відстежує статус мутації, а не прогрес завантаження у байтах.

    Деталі гранулярності скасування даних

    Занадто вузька гранулярність: оновлюється список, але ігноруються деталі → сторінка з деталями застаріла. Занадто широка гранулярність: кожен елемент під ['menu'] оновлюється заново → значні навантаження. Підбирайте гранулярність відповідно до екранів, які можуть показувати неузгодженості. У разі сумнівів скасуйте дані списку та ідентифікатор деталей, який ви змінили. Префікс скасування призначений для навмисного поширення змін.

    Підводні камені оптимістичних оновлень

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

    Strict Mode у React та подвійне монтування

    Під час розробки Strict Mode двічі викликає ефекти. TanStack Query усуває дублікати за ключем, тому ви не повинні бачити подвійних запитів до мережі для одного й того ж ключа під час виконання. Якщо це трапляється, ваш ключ є нестабільним або проблеми з ідентичністю queryFn порушують припущення щодо усунення дублікатів. Логуйте ключі під час дебаггінгу.

    Примітки щодо SSR та гідратації

    Для Next.js та подібних фреймворків необхідно дегідратувати клієнтський модуль запитів на сервері та гідратувати його на клієнті, щоб інструменти типу Notepad залишалися функціональними під час навігації. Переконайтеся, що функції queryFn виконуються у обох середовищах, або використовуйте функцію server-prefetch для заповнення кешу перед відображенням контенту. Невідповідність форматів даних між сервером та клієнтом спричиняє попередження про проблеми з гідратацією, які виглядають як баги фреймворку, але насправді є проблемами з ініціалізацією кешу.

    Порівняння з ручно створеними шаблонами SWR

    Багато команд переосмислюють окремі аспекти цієї бібліотеки: кеш-мапи, автоматичне оновлення даних, функції мутації та перевірки актуальності даних. TanStack Query стандартизує ці підходи за допомогою стандартних налаштувань спільноти та інструментів Devtools. Ручна реалізація є доцільною лише для дуже малих додатків або незвичайних середовищ виконання. В інших випадках перевагу має використання готових рішень через економію часу.

    Інструменти Devtools та можливості моніторингу

    Інструменти розробки React Query відображають ключі, ступінь застарілості даних, спостерігачі та статус запиту. Навчіть команду читати цю інформацію перед додаванням записів у консоль. У продакшені видаляйте конфіденційні дані з звітів про помилки; у разі необхідності зберігайте інформацію про помилки запитів разом із назвами ключів, а не повними даними.

    Чек-лист антипатернів

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

    Короткий огляд для тих, хто швидко читає

    Офіціанти здійснюють транспортування. Менеджери пам’ятають та координують дії. Ключі записуються на сторінках блокнота. Перемикачі свіжості контролюють можливість повторного використання. Мутації записують зміни. Недійсність та оптимізм забезпечують чесність блокнота. Ось що таке TanStack Query у короткому викладі — і чому він обгортає Axios, а не замінює його.

    Додаткові вправи для практики на кухні

    Побудуйте невеликий додаток: запит списку, запит деталей, створіть мутацію з недійсністю, а потім оптимістичне створення з примусовим шляхом помилки. Додайте функцію виходу, яка очищує клієнта. Додайте попереднє завантаження при наведенні курсору на елемент списку. Вимірюйте мережеві виклики у Devtools до та після використання спільних ключів. Ці вправи краще допомагають зрозуміти бібліотеку, ніж просте читання таблиць API.

    Під час перегляду PR-запитів запитуйте: який є ключ? Що таке staleTime та чому він існує? Що стає недійсним після запису? Чи запобігає UI у статусі pending подвійним надсиланням даних? Якщо відповіді чіткі, функція буде правильно працювати під тиском конкурентності та навігації.

    Шаблони попереднього завантаження, які здаються миттєвими

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

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

    Коли деталі потребують ідентифікатора зі списку вибору, використовуйте фільтр з enabled: !!selectedId. Якщо другий запит потребує даних від першого, обережно поєднуйте їх: або вкладіть другий ключ у ідентифікатор першого результату, або використайте єдину функцію queryFn, яка повертає обидві структури, якщо це підтримується API. Послідовні запити погіршують час відгуку; паралельні запити з спільними заголовками автентифікації є кращим варіантом, якщо це дозволяє незалежність.

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

    Сторінкування, нескінченні запити та кешування сторінок

    У списках сторінок часто використовуються параметри сторінки у ключі: ['orders', { page, pageSize, status }]. Зміна сторінки створює нову запис у кеші; щоб таблиця не залишалась порожньою, зберігайте попередні дані за допомогою placeholderData: keepPreviousData (або еквівалента в поточному API). Нескінченні запити додають нові сторінки; обережно анулюйте їх, щоб не випадково не скинути позицію прокрутки. Коли мутація змінює один рядок, виправте відповідну запис сторінки або анулюйте всю список залежно від чутливості до порядку сортування.

    Відновлення помилок та інтерфейс повторної спроби

    Стандартні спроби повторного запуску допомагають у ситуаціях з нестабільними мобільними мережами. Для кодів помилок 401/403 вимкніть спроби повторного запуску та перенаправте користувача на вход. У разі коду помилки 404 при перегляді деталей — негайно поверніть помилку. Для досвідчених користувачів відображайте значення failureCount та failureReason у вікнах підтримки. Слухачі глобального QueryCache можуть відображати сповіщення про помилку один раз на ключ, а не один раз на кожного спостерігача — це допоможе уникнути надмірної кількості сповіщень, коли п’ять компонентів використовують однаковий некоректний запит.

    Стратегії тестування

    У одиничних тестах використовуйте новий об’єкт QueryClient із параметром retry: false та короткий цикл збирання сміття. Використовуйте моки для функції queryFn або інструменти типу MSW. Перевіряйте стани завантаження, успіху та помилки. Для операцій змін переконуйтесь, що функція invalidateQueries була викликана з потрібним ключем. Інтеграційні тести мають перевіряти, що два компоненти, які використовують один і той самий ключ, не виконують подвійний запит. Нестабільні тести часто виникають через залишки кешу між тестами — створюйте новий клієнт для кожного тесту.

    Оновлення версій та зміни API

    Pри оновленні TanStack Query від версії v4 до v5 змінили назви деяких параметрів та значення стандартних налаштувань. Перед оновленням прочитайте посібник з міграції, оновіть пакет Devtools та ще раз перевірте використання параметрів keepPreviousData / placeholderData. Фіксуйте версії у файлах lockfiles. Зміни форми ключів запитів вважайте проблемними: старі кеші можуть не відповідати новим ключам після розгортання — прийміть можливість одноразового використання «холодного» кешу або змініть префікс ключа.

    Примітки з інцидентів у продакшені

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

    Документуйте стандартні налаштування вашого сервісу: значення за замовчуванням для staleTime, які запити є прив’язаними до користувача, як відбувається очищення стану після вихіду з облікового запису та коли дозволяються оптимістичні оновлення. Єдності важливіше, ніж кмітливість.

    Нотатки з інцидентів у продакшені

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

    Документуйте стандартні налаштування вашого сервісу: значення за замовчуванням для staleTime, які запити є прив’язаними до користувача, як відбувається очищення стану після вихіду з облікового запису та коли дозволяються оптимістичні оновлення. Єдності важливіше, ніж кмітливість.

    Нотатки з інцидентів у продакшені

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

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