Подробное руководство по TanStack Query: запросы, кэш, изменения данных и оптимистичный интерфейс.
TanStack Query управляет состоянием сервера как менеджер ресторана: общие ключи кэша, настройки свежести данных, скоординированные изменения, аннулирование старых данных и оптимистичные обновления, работающие на основе вашего HTTP-клиента.
Сегодняшняя тема — 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'], менеджер отправляет соответствующий запрос. Когда несколько секунд спустя загружается второй компонент с тем же ключом, он читает данные из блокнота, вместо того чтобы снова выполнять запрос. Именно благодаря этой дедупликации панели управления с большим количеством карточек, использующих общие запросы к данным пользователя или конфигурации, работают быстро без необходимости в специальных глобальных хранилищах.
Менеджер в действии (шаг за шагом)
- Загружается компонент A → не найдено в кэше → запрос в сеть.
- Приходит ответ → запись в кэш → отрисовка компонента A.
- Загружается компонент B с тем же ключом → находится в кэше → немедленная отрисовка компонента B.
- Согласно правилам свежести, позже может произойти фоновый повторный запрос без блокировки первой отрисовки компонента 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:
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. Миграция происходит поэтапно: работа с одним экраном за раз позволяет быстрее избежать ошибок конкуренции.
Итоговый чек-лист перед выпуском функции
- Стабильный, иерархический
queryKey. - Явно указанный
staleTimeдля домена. - Мутация в сочетании с аннулированием или оптимизмом.
- Временно отключённый интерфейс предотвращает повторную отправку данных.
- Подключены уведомления об ошибках и границы работы.
- Ключи, связанные с аутентификацией, очищаются по окончании сессии.
Если соблюдены все шесть пунктов, 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 и подобных фреймворков необходимо деинициализировать клиентскую часть запросов на сервере и инициализировать её на клиенте, чтобы функционал не терялся при навигации. Убедитесь, что функции queryFn выполняются в обоих средах, или используйте функцию server-prefetch для заполнения кэша перед отрисовкой. Различия в структуре данных между сервером и клиентом приводят к предупреждениям о процессе инициализации, которые выглядят как ошибки фреймворка, но на самом деле являются ошибками формирования кэша.
Сравнение с реализациями SWR вручную
Многие команды переосмысливают отдельные аспекты этой библиотеки: кэш-маппинг, автоматическое обновление данных при смене фокуса, методы mutate+revalidate. TanStack Query стандартизирует эти подходы с помощью общепринятых настроек и инструментов Devtools. Реализация вручную оправдана только для крайне маленьких приложений или нестандартных сред выполнения. В остальных случаях преимущество явно на стороне готовых решений, сокращающих время разработки.
Инструменты Devtools и возможности отслеживания
Инструменты разработчика React Query отображают ключи, степень устаревания данных, обозреватели и статус запросов. Научите команду читать эти данные перед добавлением записей в консоль. В производственной среде удаляйте конфиденциальные данные из отчетов об ошибках; при необходимости соблюдения конфиденциальности записывайте информацию об ошибках запросов с указанием имен ключей, а не полных данных загрузки.
Чек-лист антипаттернов
Нестабильные ключи; отсутствие процедуры аннулирования данных; бесконечное устаревание без синхронизации изменений; сохранение флагов интерфейса в серверной кэше; слишком широкое аннулирование данных; оптимистичные обновления без возможности отката; игнорирование параметра enabled до появления идентификаторов; использование операций изменения для чтения данных. Избегайте этих подходов, и порядок в системе сохранится.
Краткое резюме для тех, кто спешит
Официанты осуществляют транспортировку. Менеджеры запоминают и координируют действия. Ключи указаны на страницах блокнота. Регуляторы свежести контролируют повторное использование. Мутации записываются. Аннулирование и оптимизм сохраняют честность блокнота. Вот TanStack Query в одном предложении — и почему он использует Axios вместо того, чтобы заменить его.
Дополнительные упражнения для практики
Соберите небольшое приложение: запрос списка, запрос деталей, создание мутации с аннулированием, затем оптимистичное создание с принудительным путем ошибки. Добавьте функцию выхода, которая очищает клиентскую часть. Добавьте предзагрузку при наведении на элемент списка. Измеряйте сетевые запросы в Devtools до и после использования общих ключей. Эти упражнения лучше помогают освоить библиотеку, чем простое чтение таблиц API.
При рассмотрении PR задавайте себе вопросы: каков ключ? Что такое staleTime и почему он нужен? Что аннуливается после записи? Предотвращает ли функция pending UI двойные отправки данных? Если ответы на эти вопросы четкие, функция будет корректно работать при одновременных операциях и нагрузках на навигацию.
Шаблоны предзагрузки, создающие ощущение мгновенности
Предзагружайте данные при наведении курсора на маршрут, при фокусировке вкладки до нажатия пользователя или после входа для стандартного запроса к панели управления. Предзагрузка заполняет кэш без необходимости использования обозревателя изменений. Когда пользователь навигирует, функция useQuery находит уже загруженные данные и пропускает отображение индикатора загрузки. Если предзагрузить неверный ключ, это приведет к расходу диапазона частот; если же предзагрузить правильный ключ, производительность улучшится без изменения кода функции queryFn.
Сочетайте предзагрузку с реалистичным значением staleTime. Предзагрузка данных, которые сразу становятся устаревшими, запускает мгновенную фоновую перезагрузку — это все еще лучше, чем полная загрузка с нуля, но не бесплатно. Предзагружайте данные, критичные для основного потока работы; редкие экраны настроек оставьте без вмешательства.
Зависимые запросы и контроль выполнения
Когда деталям требуется идентификатор из списка выбора, используйте фильтр с параметром enabled: !!selectedId. Если второй запрос нуждается в данных первого, тщательно организуйте их связь: либо вложите второй ключ в идентификатор результата первого запроса, либо используйте единую функцию запроса, возвращающую оба формата данных, если API это поддерживает. Последовательные запросы увеличивают время отклика; при наличии возможности предпочтительнее использовать параллельные запросы с общими заголовками авторизации.
Режим Suspense влияет на способ формирования границ загрузки. Если команда использует Suspense, синхронизируйте границы обработки ошибок и убедитесь, что ошибки запросов возникают так, как ожидается. Сочетание Suspense и классических флагов загрузки сбивает с толку проверяющих — выбирайте один стиль для каждой иерархии маршрутов.
Пагинация, бесконечные запросы и кэширование страниц
При отображении списков страницы часто используют параметры страницы в виде ключа: ['orders', { page, pageSize, status }]. Изменение страницы создаёт новую запись в кэше; чтобы таблица не оставалась пустой, сохраняйте предыдущие данные с помощью placeholderData: keepPreviousData (или эквивалента в текущем API). Бесконечные запросы добавляют новые страницы; необходимо осторожно аннулировать старые записи, чтобы избежать непредвиденного сброса позиции прокрутки. Когда изменения касаются одной строки, исправляйте соответствующую запись страницы или аннулируйте всю список в зависимости от чувствительности сортировки.
Восстановление после ошибок и пользовательский интерфейс повторных попыток
Стандартные попытки повтора помогают справиться с нестабильными мобильными сетями. Для кодов ошибок 401/403 отключите попытки повтора и перенаправьте на вход. При коде ошибки 404 при отображении деталей сразу сообщите об ошибках. Для продвинутых пользователей отображайте значения failureCount и failureReason в подсказках поддержки. Слушатели глобального QueryCache могут выводить уведомления об ошибках один раз на ключ, а не по одному уведомлению на каждого наблюдателя — это поможет избежать перегрузки уведомлениями, когда пять компонентов используют один и тот же несрабатывающий запрос.
Стратегии тестирования
В единичных тестах используйте новый объект QueryClient с параметром retry: false и короткую процедуру сбора мусора. Имитируйте функцию queryFn или используйте MSW. Проверяйте состояния загрузки, успеха и ошибок. Для мутаций убедитесь, что была вызвана функция invalidateQueries с ожидаемым ключом. Интеграционные тесты должны проверять, что два компонента, использующие один и тот же ключ, не выполняют двойную загрузку данных. Нестабильные тесты часто возникают из-за остатков кэша между тестами — создавайте новый клиент для каждого теста.
Обновления версий и смещение API
При обновлении TanStack Query с версии v4 на v5 были изменены названия некоторых опций и скорректированы значения по умолчанию. Перед обновлением прочитайте руководство по миграции, обновите пакет Devtools и снова проверьте использование параметров keepPreviousData / placeholderData. Фиксируйте версии в файлах lockfiles. Изменения формы ключей запросов считайте проблемными: старые кэши могут не соответствовать новым ключам после развертывания — примите возможность временного использования кэша или измените префикс ключа.
Заметки с мест производства о инцидентах
Одна команда столкнулась с дублированием запросов типа POST, поскольку кнопка отправки оставалась активной, пока значение isPending было истинным в другом экземпляре мутации. Другая команда по ошибке очистила весь кэш при выходе из системы, создав нового клиента без сброса старой ссылки на поставщика данных. Третья команда закодировала объекты пользователей в ключах, что нарушило возможность совместного использования структур. Запишите эти примеры в документацию для новичков, чтобы они избегали аналогичных проблем, не открывая их заново.
Документируйте стандартные настройки вашей системы: значение по умолчанию для staleTime, какие запросы являются локальными для пользователя, как происходит сброс состояния при выходе из системы и когда допускаются оптимистичные обновления. Согласованность важнее изобретательности.
Заметки с мест происшествий в продакшене
Одна команда столкнулась с дублированием запросов типа POST, поскольку кнопка отправки оставалась активной, пока значение isPending было истинным в другом экземпляре мутации. Другая команда по ошибке очистила весь кэш при выходе из системы, создав нового клиента без сброса старой ссылки на поставщика данных. Третья команда закодировала объекты пользователей в ключах, что нарушило возможность совместного использования структур. Запишите эти примеры в документацию для новичков, чтобы они избегали аналогичных проблем, не открывая их заново.
Документируйте стандартные настройки вашей системы: значение по умолчанию для staleTime, какие запросы являются локальными для пользователя, как происходит сброс состояния при выходе из системы и когда допускаются оптимистичные обновления. Согласованность важнее изобретательности.
Заметки с мест происшествий в продакшене
Одна команда столкнулась с дублированием запросов типа POST, поскольку кнопка отправки оставалась активной, пока значение isPending было истинным в другом экземпляре мутации. Другая команда по ошибке очистила весь кэш при выходе из системы, создав нового клиента без сброса старой ссылки на поставщика данных. Третья команда закодировала объекты пользователей в ключах, что нарушило возможность совместного использования структур. Запишите эти примеры в документацию для новичков, чтобы они избегали аналогичных проблем, не открывая их заново.
Документируйте стандартные настройки вашей системы: значение по умолчанию для staleTime, какие запросы являются локальными для пользователя, как происходит сброс состояния при выходе из системы и когда допускаются оптимистичные обновления. Согласованность важнее изобретательности.