TanStack Query во Vue: данные API и кеш — мастерская и 8 живых примеров
Восемь задач на настоящем Vue Query: общий кеш, фоновые обновления, фильтры, пагинация, зависимые запросы и откат изменений. Мастерская и исходники с PHP API для Windows/Linux.

TanStack Query во Vue: восемь живых задач с кешем, фоновым обновлением, пагинацией, зависимыми запросами, отменой и откатом изменения. Мастерская и исходники с PHP API.
TanStack Query во Vue: данные API без потери контекста
В кабинете пользователя список заявок загружается с сервера. Пока запрос идёт, нужен понятный индикатор. После ответа — данные. При ошибке — объяснение и повтор. Если список уже был открыт, хочется показать его сразу, затем аккуратно обновить. Добавьте фильтры, пагинацию и изменение статуса, и простой fetch постепенно обрастёт состояниями, которые приходится согласовывать вручную.
TanStack Query помогает организовать именно этот слой: асинхронные данные, кеш, повторное чтение, ошибки и изменения. Во Vue используется пакет @tanstack/vue-query. Он не рисует за вас таблицу и не создаёт сервер; библиотека даёт состояние запроса и правила его жизни, а интерфейс решает, как показать это человеку.
В статье восемь практических сценариев на одном кабинете заявок. Можно менять задержку, вызывать ошибку, наблюдать общий кеш, перелистывать страницы и проверять откат изменения. В исходниках дополнительно есть настоящий PHP API, поэтому после эксперимента в браузере вы сможете повторить тот же путь с HTTP на Windows или Linux.
Где применять: админки, каталоги, личные кабинеты и CRM
Библиотека полезна в интерфейсах, которые постоянно читают удалённые данные: список заказов, карточка клиента, история платежей, каталог, поиск, настройки пользователя. Особенно заметна польза, когда один ресурс показывают несколько компонентов или пользователь возвращается к уже открытой странице. Ему не нужно каждый раз начинать с пустого экрана, если приложение может показать прежний результат и отдельно сообщить об обновлении.
В CRM на Vue такой подход подходит спискам заявок, карточкам и справочникам. При этом выбранная вкладка, открытое меню и незавершённый ввод формы остаются локальным состоянием интерфейса. Их не обязательно отправлять в кеш запросов только потому, что библиотека уже установлена.
Начинайте с законченного пользовательского пути: открыть список, выбрать фильтр, перейти к клиенту и изменить одну запись. На нём легко проверить, исчезают ли строки во время обновления, не подменяет ли старый ответ новый фильтр и понятно ли поведение после ошибки. Общий замысел библиотеки изложен в официальном обзоре.
Чем отличается от fetch, Axios и обычного store
fetch выполняет HTTP-запрос и возвращает Promise. Axios решает похожую транспортную задачу со своим интерфейсом. TanStack Query работает уровнем выше: вызывает вашу queryFn, сохраняет результат по ключу, отслеживает его состояние и решает, когда нужно повторное получение. Поэтому библиотека не требует отказаться от выбранного HTTP-клиента.
Обычный store удобен для локальных решений приложения. Кеш удалённых данных сложнее: сервер может измениться без участия этой вкладки, запросы заканчиваются в разном порядке, а два компонента способны одновременно попросить один ресурс. Если записать всё это в один массив store, потребуется самостоятельно определить свежесть, дедупликацию, отмену и ошибки каждого запроса.
Однако установка Query не делает API правильным автоматически. Ответы всё ещё нужно проверять, авторизацию выполнять на сервере, а состояние формы не затирать фоновым обновлением. Вы получаете инструмент организации асинхронных данных; границы продукта и его бизнес-правила остаются явными решениями разработчика.
async function getTickets({ signal }) {
const response = await fetch('/api/tickets', { signal });
if (!response.ok) throw new Error('HTTP ' + response.status);
return response.json();
}
// Эта функция может стать queryFn; HTTP выполняет fetch.
QueryClient, query key и наблюдатель компонента
QueryClient владеет кешем. Query key определяет, какие данные в нём лежат: например, список заявок с конкретной страницей и фильтром. useQuery связывает компонент Vue с соответствующим запросом. Несколько наблюдателей могут смотреть на один ключ и получать согласованное состояние без отдельных независимых копий массива.
Для обычного клиентского приложения создавайте один стабильный QueryClient на его жизненный цикл. Не создавайте новый клиент при каждом рендере компонента: так кеш будет постоянно теряться. Для серверного рендеринга правила изоляции другие — клиент и чувствительные данные должны быть ограничены конкретным запросом пользователя, а не общей глобальной переменной процесса.
Мастерская показывает ключи, состояние данных, состояние чтения и число наблюдателей. Это учебная диагностика, позволяющая понять связь между компонентами. В реальном продукте посетителю обычно нужны понятные подписи вроде «Обновляем» и «Повторить», а внутренние ключи остаются инструментом разработчика.
import { createApp } from 'vue';
import { QueryClient, VueQueryPlugin } from '@tanstack/vue-query';
const queryClient = new QueryClient();
const app = createApp(App);
app.use(VueQueryPlugin, { queryClient });
app.mount('#app');
Мини-мастерская: восемь задач в кабинете заявок
На странице работает настоящий Vue Query. Управляемый учебный API хранит восемнадцать заявок в памяти вкладки и возвращает Promise с выбранной задержкой. Переключатели позволяют воспроизвести ошибку, пустой ответ и отказ изменения. Это не HTTP-запросы к серверу сайта: механизм указан прямо в интерфейсе, чтобы вы не принимали учебный журнал за сетевую телеметрию.
Начните с загрузки, затем включите второй виджет, измените фильтр и перейдите на следующую страницу. В сценарии свежести поменяйте данные внутри учебного API и отдельно обновите кеш. В сценарии мутации включите отказ и попробуйте завершить заявку — статус сначала изменится, затем вернётся после ошибки.
Можно скачать полученный JSON и небольшой useQuery-фрагмент с выбранными настройками. Полный проект содержит ещё и PHP endpoint для настоящего HTTP-режима. Для удобства на телефоне мастерская открывается отдельной страницей; встроенная версия остаётся доступной непосредственно рядом с объяснениями.
Версии и важные настройки этого примера
В проекте закреплены Vue 3.5.45, @tanstack/vue-query 5.107.1 и Bootstrap 5.3.8. Сборку выполняет esbuild 0.28.2. Зависимости зафиксированы package-lock.json, поэтому готовый архив устанавливается через npm ci. Все браузерные ресурсы лежат локально; работа мастерской не зависит от случайно изменившейся ссылки на CDN.
Некоторые настройки выбраны специально для управляемого опыта. По умолчанию здесь нет автоматического повтора чтения, возврат фокуса не запускает refetch, а неиспользуемые запросы хранятся шестьдесят секунд. Эти значения не следует объявлять универсальными настройками любого приложения. В интерфейсе отдельно можно поменять свежесть и один повтор чтения.
Перед переносом в рабочий проект изучите важные значения по умолчанию. Нередко неожиданный запрос после возвращения на вкладку — предусмотренное поведение библиотеки. Полезнее сначала выбрать нужное продуктовое поведение, чем отключать все механизмы обновления после первого удивления.
const queryClient = new QueryClient({
defaultOptions: {
queries: { retry: 0, gcTime: 60_000, refetchOnWindowFocus: false }
}
});
Windows: запуск мастерской и HTTP-режима с PHP
Установите поддерживаемый Node.js 24 LTS не ниже 24.15, распакуйте архив и откройте PowerShell в каталоге tanstack-query-project. Команды ниже восстанавливают зависимости, собирают страницу и запускают проверки. Обычная мастерская открывается на http://127.0.0.1:8300/. Для остановки нажмите Ctrl+C в её терминале.
Настоящий HTTP-режим требует PHP 8.2 или новее. В отдельном терминале запустите встроенный PHP-сервер из корня проекта на loopback-порту 8305. Откройте public/index.html с параметром api=php. Теперь список и изменения действительно проходят через server/api.php; искусственные переключатели задержки и отказов отключены.
Перезагрузка обычной страницы создаёт новый in-memory опыт. В PHP-режиме учебные данные хранятся в сессии и переживают перезагрузку страницы. Кнопка «Вернуть пример» там сбрасывает клиентский кеш, но не уничтожает серверную сессию. Это различие важно при сравнении результата, а в интерфейсе есть отдельное пояснение.
node --version
npm.cmd ci --ignore-scripts
npm.cmd run build
npm.cmd test
npm.cmd start
# Во втором окне, из корня проекта:
php -S 127.0.0.1:8305 -t .
# http://127.0.0.1:8305/public/index.html?api=php
Linux и VPS: от локальной проверки к рабочему размещению
На Linux используется тот же lock-файл и исходники. Установите Node.js 24 LTS и подходящий PHP средствами своей системы, затем выполните сборку и проверки обычным пользователем. Встроенные серверы из примера нужны для разработки и слушают только 127.0.0.1. Они позволяют проверить контракт до настройки домена и внешнего доступа.
Если опыт выполняется на VPS, для первого просмотра используйте SSH-туннель: локальный порт рабочего компьютера перенаправляется на loopback сервера. После этого браузер открывает привычный адрес 127.0.0.1. Не требуется публиковать порт PHP dev server в интернет и менять firewall ради учебного запуска.
Для постоянного размещения готовую папку public обслуживает существующий веб-сервер, а PHP endpoint — PHP-FPM. Node при этом нужен на этапе сборки, а не обязательно как постоянно работающий процесс. Ниже разобраны точный маршрут API, document root и права каталога сессий, чтобы пример можно было применить за пределами Windows.
node --version
php --version
npm ci --ignore-scripts
npm run build
npm test
node tools/test-php.mjs
php -S 127.0.0.1:8305 -t .
# С рабочей машины:
ssh -L 8305:127.0.0.1:8305 user@your-server
Пример 1. Загрузка, ошибка и пустой результат
Первое чтение ещё не имеет данных. Интерфейс показывает загрузку, затем список или объяснение ошибки. Пустой успешный ответ — отдельный случай: сервер ответил корректно, но по условиям нет записей. Если смешать его с ошибкой, пользователь начнёт повторять уже успешный запрос вместо изменения фильтра.
В мастерской выберите задержку, включите ошибку следующего чтения и нажмите обновление. Для чистого наблюдения сначала оставьте автоматический повтор выключенным. Затем выполните ручной повтор. Переключатель пустого списка действует, пока вы его не отключите; это позволяет спокойно рассмотреть пустое состояние и поведение пагинации.
Для HTTP queryFn должна превращать неуспешный статус в отклонённый Promise. Сам fetch не считает любой HTTP 4xx или 5xx сетевой ошибкой. Если забыть проверку response.ok, библиотека может получить JSON ошибки как успешные данные. В реальном проекте дополнительно проверяйте форму ответа, чтобы неправильная структура не дошла до отображения.
const tickets = useQuery({
queryKey: ['tickets', { page: 1, filter: 'all' }],
queryFn: async ({ signal }) => {
const response = await fetch('/api/tickets?page=1&filter=all', { signal });
if (!response.ok) throw new Error('Не удалось получить заявки');
return response.json();
},
retry: 0
});
Состояние данных и состояние запроса — две оси
Наличие данных и активность запроса — разные вещи. Список может успешно отображаться, пока в фоне идёт новое чтение. Запрос также может стоять на паузе из-за сетевого режима. Поэтому одного флага loading часто недостаточно для понятного интерфейса.
В мастерской видны pending, success и error как состояние данных, а fetching, paused и idle — как состояние выполнения. Первая загрузка отличается от фонового обновления: во втором случае прежние строки сохраняются. Если повторное чтение завершилось ошибкой, полезный предыдущий результат можно оставить на экране и отдельно объяснить, что он не обновлён.
Не превращайте внутренние названия в обязательный текст для посетителя. Он должен понимать действие: «Загружаем первый список», «Обновляем данные», «Соединение приостановлено», «Не удалось обновить, повторите». Технические поля удобны в панели разработчика, но смысл каждого состояния лучше описать словами, связанными с задачей человека.
// В setup() это refs, поэтому читаем .value:
const firstLoad = tickets.isLoading.value;
const backgroundRefresh = tickets.isFetching.value && !!tickets.data.value;
const paused = tickets.fetchStatus.value === 'paused';
const previousData = tickets.data.value;
Пример 2. Один ключ — общий кеш для двух виджетов
Покажите второй виджет с тем же query key. Если данные свежие, дополнительное чтение не требуется. Если запрос ещё выполняется, наблюдатели одного запроса используют общую работу. Это удобно, когда список, счётчик и небольшая панель одновременно нуждаются в одном ресурсе.
Условие — одинаковая семантика ключа и один QueryClient. Не используйте один ключ для ответов разной структуры: например, для массива списка и отдельной карточки. В таком случае библиотека не может догадаться, что одинаковое имя скрывает несовместимые данные.
Мастерская показывает число наблюдателей, включая отключённого наблюдателя второго виджета. Это диагностическое число не равно количеству HTTP-запросов. Смотрите также журнал вызовов учебного API: он помогает различить подписку компонента, попадание в кеш и фактический новый вызов функции чтения.
const commonOptions = {
queryKey: ['tickets', { page: 1, filter: 'all' }],
queryFn: getTickets,
staleTime: 30_000
};
const first = useQuery(commonOptions);
const second = useQuery(commonOptions);
Пример 3. staleTime: свежесть без таймера опроса
staleTime определяет, сколько времени результат считается свежим. Он не запускает HTTP через каждые несколько секунд. После устаревания запрос может обновиться по предусмотренному событию или явному действию. Если нужен периодический опрос, для него выбирают отдельную настройку и отдельно оценивают нагрузку.
Нажмите «Изменить данные в учебном API». Это меняет исходное название первой заявки, но не кеш компонента. Затем выполните обновление: прежний список остаётся видимым, пока новый ответ находится в пути. Так можно увидеть разницу между источником данных и локальным снимком, которым сейчас пользуется интерфейс.
Выбирайте свежесть по задаче. Справочник, который меняется редко, и оперативный статус обработки требуют разных правил. Слишком короткое время увеличивает число чтений, а слишком длинное может показывать устаревшую информацию. Удобный интерфейс при этом объясняет время последнего обновления и оставляет человеку понятную возможность запросить свежие данные.
const tickets = useQuery({
queryKey: ['tickets'], queryFn: getTickets,
staleTime: 30_000,
refetchOnWindowFocus: false
});
await tickets.refetch(); // явное обновление независимо от ожидания таймера
gcTime: когда удаляется неиспользуемый кеш
Свежесть ответа и срок хранения неиспользуемого запроса решают разные задачи. gcTime задаёт задержку очистки после ухода последних наблюдателей запроса. Один лишь enabled: false не размонтирует наблюдателя и не запускает такую очистку. Это не срок жизни активной таблицы и не обещание удалить данные ровно через минуту после получения ответа.
В нашей мастерской значение уменьшено до шестидесяти секунд, чтобы опыт не оставлял длинную историю неиспользуемых ключей. Пока пользователь активно работает со списком, этот параметр не должен восприниматься как принудительный сброс каждые шестьдесят секунд. После возвращения к удалённому запросу потребуется новое получение.
В продукте проверьте типичный путь навигации: человек открывает карточку, возвращается в список и ожидает прежний фильтр и данные. Достаточное время хранения делает этот путь спокойнее, но большой набор ключей и объём ответов требуют контроля памяти. Не сохраняйте в кеше лишние поля только потому, что сервер умеет их вернуть.
const client = new QueryClient({
defaultOptions: { queries: { staleTime: 30_000, gcTime: 60_000 } }
});
// staleTime: свежесть; gcTime: очистка неактивного запроса.
Пример 4. Query keys: страница и фильтр входят в идентичность
Если ответ зависит от фильтра, фильтр должен участвовать в ключе. То же относится к странице, поисковой строке, сортировке и другим параметрам, меняющим результат. Иначе разные ответы попадут в одну запись кеша и будут подменять друг друга.
В примере ключ строится из tickets и объекта с page и filter. queryFn берёт параметры из queryKey текущего запроса, а не из произвольного поздно прочитанного замыкания. Это делает связь между адресом кеша и отправленными условиями явной. При смене фильтра страница возвращается к первой, чтобы старый номер не уводил пользователя за конец короткого списка.
Поставьте длинную задержку и быстро переключайте статусы. Результат старого фильтра не должен стать новым списком. Отмена и разные ключи работают совместно: один механизм прекращает ненужное ожидание, другой разделяет результаты. Правила ключей подробно разобраны в официальном руководстве.
const options = computed(() => ({
queryKey: ['tickets', { page: page.value, filter: filter.value }],
queryFn: ({ signal, queryKey }) => api.list({ ...queryKey[1], signal })
}));
const tickets = useQuery(options);
Пример 5. Пагинация: оставить прежние строки до ответа
При переходе на следующую страницу не всегда удобно мгновенно очищать таблицу. keepPreviousData позволяет временно показать предыдущий результат, пока новый находится в пути. Пользователь сохраняет контекст, но интерфейс обязан обозначить, что это пока прежние строки.
Для этого используется isPlaceholderData. В мастерской появляется пояснение, а действия над показанными строками временно блокируются. Иначе человек мог бы нажать «Завершить» на записи предыдущей страницы, думая, что уже видит новую. Кнопки перехода также учитывают границы списка и текущее ожидание.
Проверьте последнюю страницу, пустой фильтр и быстрое возвращение назад. Сохранение прежнего результата — способ сделать ожидание спокойнее, а не разрешение скрыть факт загрузки. Описание подхода с keepPreviousData есть в разделе пагинации; в исходниках отдельно проверяется переход от первой страницы ко второй.
import { keepPreviousData } from '@tanstack/vue-query';
const tickets = useQuery({
queryKey: computed(() => ['tickets', { page: page.value, filter: filter.value }]),
queryFn: ({ signal, queryKey }) => api.list({ ...queryKey[1], signal }),
placeholderData: keepPreviousData
});
// Пока tickets.isPlaceholderData.value, не изменяем прежние строки.
Пример 6. Зависимый запрос: сначала выбрать клиента
Карточка клиента не нужна, пока пользователь никого не выбрал. enabled связывает выполнение запроса с этим условием. Пустая строка в ключе сама по себе не заменяет такое решение: функция чтения всё ещё могла бы попытаться вызвать API с неверным параметром.
Выберите Атлас, Контур или Маяк. Появится состояние загрузки и карточка выбранного клиента. Затем очистите выбор: интерфейс должен явно сообщить, что запрос не запускается. Старые данные в кеше могут оставаться, но их не нужно выдавать за текущий выбор пользователя.
При цепочках из нескольких запросов следите за последовательным ожиданием: иногда API можно спроектировать так, чтобы избежать длинного каскада. enabled полезен для действительно зависимого действия, но не заменяет анализ контракта сервера. Базовый механизм описан в Dependent Queries.
const selected = ref('');
const details = useQuery(computed(() => ({
queryKey: ['client', selected.value],
queryFn: ({ signal, queryKey }) => api.client(queryKey[1], signal),
enabled: Boolean(selected.value),
staleTime: 30_000
})));
Пример 7. Mutation: быстро показать изменение
Нажатие «Завершить» меняет состояние заявки на сервере или в учебном API. Для этого используется useMutation. Оптимистичный интерфейс сначала показывает ожидаемый результат, затем ждёт подтверждения. Такой подход делает простое обратимое действие отзывчивым, но требует честной обработки отказа.
В мастерской onMutate сначала отменяет чтения списков, чтобы старый ответ не затёр ожидаемое изменение. Затем сохраняет снимки кеша и обновляет нужную строку. Пользователь видит подпись, что подтверждение ещё ожидается. Пока операция идёт, остальные изменения заблокированы: пример не пытается скрыть сложность конкурирующих мутаций.
Выбирайте оптимистичность по последствиям. Отметка учебной задачи и необратимое списание средств имеют разные требования. Для важных операций лучше дождаться серверного подтверждения и явно показать промежуточный статус. Библиотека помогает организовать цикл, но не принимает за вас решение, какое действие безопасно показывать заранее.
const mutation = useMutation({
mutationFn: ({ id, status }) => api.update(id, status),
retry: 0,
onMutate: async ({ id, status }) => {
await client.cancelQueries({ queryKey: ['tickets'] });
const previous = client.getQueriesData({ queryKey: ['tickets'] });
client.setQueriesData({ queryKey: ['tickets'] },
data => optimisticRows(data, id, status));
return { previous };
}
});
Rollback: вернуть снимок после отказа
Включите отказ следующего изменения и завершите открытую заявку. Сначала статус переключится, затем ошибка вернёт сохранённое состояние. Сообщение должно объяснять результат операции, а не просто показывать красный значок: изменение не подтверждено, прежние данные восстановлены.
Снимок в примере сохраняется для всех списков tickets, которые находятся в кеше. Это важно, потому что одна запись могла быть видна в разных фильтрах и страницах. После завершения выполняется повторная сверка с источником. При фильтрованном списке промежуточная оптимистичная строка может ещё оставаться в текущем наборе до этой сверки; финальный состав определяет новый ответ API.
Простой возврат снимка предполагает последовательную мутацию. Если разрешить несколько изменений одной записи одновременно, ранний отказ может вернуть слишком старое состояние и затереть более поздний успех. Для такого продукта нужны версии, идемпотентность или более точный механизм согласования. Несколько способов оптимистичного обновления описаны в официальном руководстве.
const callbacks = {
onError: (error, variables, context) => {
for (const [key, data] of context?.previous || []) {
client.setQueryData(key, data);
}
},
onSettled: () => client.invalidateQueries({ queryKey: ['tickets'] })
};
// Передайте эти callbacks вместе с mutationFn и onMutate в useMutation.
Invalidation и данные ответа: как согласовать кеш
После успешного изменения можно обновить кеш известным ответом сервера или пометить связанные запросы требующими новой проверки. Выбор зависит от контракта. Если сервер возвращает полноценную карточку, её удобно положить в соответствующий ключ. Для сложного списка с сортировкой, фильтрами и подсчётом общего количества проще выполнить повторное чтение.
В нашей мастерской onSettled инвалидирует списки tickets и возвращает Promise этой операции. Поэтому промежуточное состояние остаётся видимым до завершения сверки. Это помогает не разблокировать действие раньше, чем интерфейс снова согласован с источником.
Продумайте область invalidation. Слишком широкая очистка всех запросов после каждого клика создаёт лишнюю нагрузку и мерцание. Слишком узкая оставит счётчик или другую страницу устаревшими. Группировка ключей по ресурсу позволяет выразить связь явно: карточка конкретной заявки, списки заявок и справочник клиентов не обязаны всегда обновляться вместе.
await client.invalidateQueries({ queryKey: ['tickets'] });
// Известный полноценный ответ можно сохранить отдельно:
client.setQueryData(['ticket', savedTicket.id], savedTicket);
Пример 8. AbortSignal: отменить ненужное чтение
Поставьте задержку 1800 мс, запустите обновление и нажмите отмену. Учебный adapter слушает AbortSignal, прекращает таймер и отклоняет ожидание. Последний подтверждённый кеш остаётся доступным. В HTTP-режиме тот же signal передаётся fetch.
Отмена чтения полезна при смене фильтра, уходе со страницы или явном решении пользователя. Но она не гарантирует отмену уже выполненного серверного действия. Сервер мог получить запрос и завершить работу до того, как браузер перестал ждать ответ. Поэтому кнопка отмены в мастерской относится именно к чтению, а не к мутации статуса.
В собственном queryFn передавайте signal дальше по цепочке. Если функция запускает несколько fetch, решите, какие из них относятся к одному отменяемому действию. Нельзя обещать пользователю экономию ресурсов только на основании скрытого результата: транспорт должен действительно поддерживать отмену. Примеры интеграции приведены в Query Cancellation.
const result = useQuery({
queryKey: ['tickets'],
queryFn: async ({ signal }) => {
const response = await fetch('/api/tickets', { signal });
if (!response.ok) throw new Error('HTTP ' + response.status);
return response.json();
}
});
await client.cancelQueries({ queryKey: ['tickets'] });
Пауза соединения и повтор чтения: разные причины ожидания
Переключатель «Пауза учебной сети» управляет onlineManager библиотеки внутри мастерской. Он не отключает Wi-Fi и не меняет сеть компьютера. Состояние paused показывает, что запрос ожидает подходящего сетевого режима. После восстановления библиотека может продолжить работу согласно выбранным правилам.
Повтор после ошибки — другой механизм. В опыте можно разрешить один автоматический повтор чтения с короткой задержкой. Поскольку управляемая ошибка расходуется первым вызовом, второй способен завершиться успешно. Поэтому при изучении ошибки полезно сначала выключить retry, а затем сравнить поведение.
Не переносите автоматический повтор на необратимую команду без серверного договора об идемпотентности. Ошибка получения ответа не всегда означает, что действие не выполнено. В примере retry мутации равен нулю. Общие сетевые режимы описаны в Network Mode; правила восстановления должны быть понятны и разработчику, и пользователю.
import { onlineManager } from '@tanstack/vue-query';
onlineManager.setOnline(false); // только режим библиотеки
onlineManager.setOnline(true);
// Чтение можно повторить; мутация в мастерской имеет retry: 0.
PHP API: настоящий запрос, сессия и проверка входных данных
Дополнительный PHP endpoint возвращает список, карточку клиента и принимает изменение статуса известной заявки. У каждой PHP-сессии свой учебный набор из восемнадцати строк. Это позволяет проверить HTTP без базы данных и без изменения общих данных сайта. Сессия здесь не является входом пользователя и не должна восприниматься как готовая система аккаунтов.
Сервер ограничивает страницу и фильтр, проверяет тип id и разрешённые статусы. POST принимает JSON до одного килобайта и отклоняет чужой Origin, если он передан. Cookie сессии имеет HttpOnly и SameSite=Strict; при невозможности открыть хранилище сессий возвращается понятная ошибка JSON со статусом 503.
В реальном API добавьте авторизацию на конкретную запись, нормальное хранилище, свою CSRF-политику и ограничения нагрузки. Не доверяйте статусу кнопки в браузере: любой клиент может отправить запрос вручную. Vue Query помогает интерфейсу работать с ответом, но решение о допустимости операции всегда остаётся на сервере.
const response = await fetch('../server/api.php?action=update', {
method: 'POST', credentials: 'same-origin',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: 2, status: 'done' })
});
const data = await response.json();
if (!response.ok) throw new Error(data.error || 'Ошибка HTTP');
Linux deployment: public directory и точный PHP-маршрут
Для размещения не нужно выдавать весь корень проекта как публичный каталог. Готовая сборка лежит в public, а server/api.php можно обслуживать отдельным точным маршрутом через PHP-FPM. Тогда package-lock, тесты и служебные файлы не превращаются в случайно доступные веб-ресурсы.
В deploy есть примеры для Caddy и Nginx. Укажите свой домен, реальный путь проекта и сокет установленного PHP-FPM. Сначала проверьте конфигурацию, затем используйте учебный режим на отдельном стенде. Каталог сессий должен быть доступен пользователю PHP-FPM, но это не повод давать запись всей папке приложения.
Начальное объяснение этих серверов есть в материалах о Caddy и Nginx. Если HTML открывается, а API возвращает текст PHP или 404, остановитесь и исправьте маршрут: клиентская библиотека не может заменить правильную серверную конфигурацию.
# Фрагмент существующего HTTPS server в Nginx
root /opt/query-demo/public;
location = /server/api.php {
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME /opt/query-demo/server/api.php;
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
}
location / {
try_files $uri $uri/ =404;
}
Формы, приватные данные и выход из аккаунта
Фоновый ответ не должен безусловно переписывать незавершённый ввод человека. Данные запроса подходят как исходное значение формы, но редактируемый черновик требует собственной модели. При новом ответе решите, показывать ли уведомление об изменениях, предлагать сравнение или обновлять только ещё не изменённые поля.
Ключи кеша также должны учитывать область данных. Если приложение переключает организацию, проект или пользователя, одинаковое tickets без контекста может быть недостаточно. При выходе из аккаунта очистите чувствительный кеш и завершите ненужные запросы. Не помещайте секреты в query key: ключи часто видны в инструментах разработчика и журналах.
Серверная проверка прав обязательна независимо от того, есть ли данные в браузерной памяти. Локальный кеш повышает удобство, но не создаёт разрешение на чтение. Для постоянного кеширования между перезапусками отдельно оцените, какие данные допустимо сохранять на устройстве и как их удалить при смене пользователя.
// Пример ключа для данных конкретной организации:
const key = ['organization', organizationId, 'tickets', { page, filter }];
// При выходе из аккаунта в рамках собственного приложения:
await queryClient.cancelQueries();
queryClient.clear();
Как проверять: гонки, rollback, HTTP и мобильный интерфейс
В проекте есть проверки общего кеша и параллельных чтений, отдельных ключей, отмены, повтора, пустого ответа, зависимого запроса и восстановления снимка после отказа. Дополнительный тест быстро меняет фильтр и убеждается, что прежний вызов отменён, а отображаемый результат соответствует новому ключу.
PHP-проверка запускает настоящий временный HTTP-сервер на свободном loopback-порту и использует отдельный каталог сессий. Она проверяет список, страницы, фильтр, изменение, изоляцию сессий и ошибочные запросы. По завершении процесс и временное хранилище удаляются. Для этой команды нужен PHP в PATH или путь в переменной PHP_BINARY.
После автоматических тестов проверьте живой интерфейс: длинную задержку, ошибку, узкий экран, смену страницы во время чтения и скачивание результата. Пока показаны placeholder-строки, кнопки изменения должны быть недоступны. Пользователь должен видеть причину ожидания и понятное следующее действие, а не разбираться во внутренних флагах библиотеки.
npm test
node tools/test-php.mjs
npm run build
npm audit --audit-level=low
Как перенести подход на свой проект
Выберите один реальный список и опишите его контракт: параметры, структура ответа, ошибки и изменение записи. Затем задайте устойчивые query keys и понятные состояния интерфейса. Не начинайте с глобальной миграции всех запросов приложения: на одном законченном пути легче увидеть правильные границы кеша и формы.
После чтения добавьте изменение с серверным подтверждением. Если оно обратимое и частое, рассмотрите оптимистичный показ, снимок и rollback. Затем проверьте конкуренцию: два быстрых действия, потерю ответа, обновление другого компонента и выход пользователя со страницы. Для необратимых операций сначала определите серверную идемпотентность.
Мастерская даёт воспроизводимую основу: восемь сценариев, управляемые условия, видимый кеш, исходники и настоящий PHP-режим. Заберите минимальный useQuery-пример, замените маршрут /api/tickets на свой API и сохраните смысл состояний. Польза инструмента проявляется в спокойном понятном интерфейсе, который остаётся пригодным для работы даже во время задержки или ошибки.


