State Management

Server-state: паттерны

Лента в Twitter догружается по мере прокрутки: дошёл до низа - подъехала следующая порция, без кнопки и без перезагрузки списка. При наведении на письмо в Gmail его содержимое подгружается заранее, и по клику оно открывается мгновенно. Поиск с фильтром по городу ждёт, пока город выбран, и не дёргает сервер впустую. Всё это не отдельные хитрости, а набор паттернов поверх одного кэша: префетч, бесконечные запросы, зависимые запросы и персист на случай офлайна. Основы Query закрывают один запрос - паттерны закрывают реальные экраны.

  • Бесконечная лента: Twitter, Instagram, ленты комментариев - useInfiniteQuery подгружает страницы по прокрутке
  • Префетч по наведению: Gmail и Linear заранее тянут содержимое письма или задачи, чтобы клик открывал мгновенно
  • Зависимые запросы: сначала профиль, по его companyId - данные компании, цепочкой через enabled
  • Пагинация в админках: страницы таблицы с keepPreviousData, чтобы при переходе не мигал пустой экран
  • Офлайн: PWA и мобильные веб-приложения персистят кэш, чтобы показывать данные без сети и доотправлять мутации позже

Предварительные знания

  • Основы TanStack Query: useQuery, queryKey, staleTime и кэш
  • Мутации и инвалидация кэша через invalidateQueries
  • Промисы, async/await и базовая работа с пагинацией на стороне API
  • TanStack Query: основы

Префетч и пагинация

Префетч кладёт данные в кэш заранее, до того как они понадобятся на экране. Метод prefetchQuery на клиенте Query берёт тот же queryKey и queryFn, что и обычный useQuery, но не подписывает компонент - просто наполняет кэш. Когда пользователь наводит курсор на ссылку или письмо, фоновый префетч тянет содержимое, и по клику useQuery с тем же ключом отдаёт готовый кэш мгновенно, без спиннера.

Постраничная пагинация ставит свою задачу: при переходе на следующую страницу ключ меняется (страница в queryKey), кэша под новый ключ ещё нет, и экран мигнул бы пустотой. placeholderData с keepPreviousData решает это: пока грузится новая страница, на экране держится предыдущая, помеченная как фоновая загрузка. Переход между страницами ощущается плавным, без скачка к пустому состоянию.

Префетч и пагинация хорошо сочетаются: на странице N можно префетчить страницу N+1, и переход вперёд откроется мгновенно из готового кэша. Тот же приём Router применяет на уровне маршрута через loaders, убирая водопад запросов при навигации.

Зачем при постраничной пагинации использовать placeholderData с keepPreviousData?

Бесконечные и зависимые запросы

Бесконечная прокрутка отличается от постраничной пагинации: страницы не заменяют друг друга, а накапливаются в один растущий список. useInfiniteQuery ведёт это как единый кэш из массива страниц. Функция getNextPageParam по последней странице вычисляет курсор следующей, а fetchNextPage подгружает её и добавляет к уже загруженным. Дошёл до низа ленты - вызвали fetchNextPage, список вырос.

Зависимый запрос нужен, когда один запрос опирается на результат другого. Запрос данных компании не может стартовать, пока не известен её companyId, а тот приходит из запроса профиля. Флаг enabled держит зависимый запрос выключенным, пока нужное значение не появилось. Это убирает запрос с undefined в параметрах и выстраивает цепочку: профиль, затем компания, затем что-то ещё по её данным.

ПаттернХук / опцияКогда применять
Постраничная пагинацияuseQuery + placeholderDataСтраницы заменяют друг друга, таблицы
Бесконечная прокруткаuseInfiniteQuery + getNextPageParamСтраницы накапливаются, ленты
Зависимый запросuseQuery + enabledОдин запрос опирается на результат другого
ПрефетчqueryClient.prefetchQueryДанные нужны заранее, по наведению или маршруту

enabled полезен шире зависимых запросов: им откладывают любой запрос до выполнения условия - выбран фильтр, открыта вкладка, дан согласие. Пока enabled равен false, запрос не идёт в сеть и остаётся в состоянии ожидания.

Запрос данных компании должен стартовать только после того, как из профиля придёт companyId. Что это обеспечивает?

Офлайн и персист кэша

По умолчанию кэш Query живёт в памяти и исчезает при перезагрузке вкладки. Для офлайна и быстрого старта кэш персистят - сохраняют во внешнее хранилище (localStorage, IndexedDB) через persistQueryClient. При следующем заходе кэш восстанавливается из хранилища, и приложение показывает данные сразу, даже без сети, а свежесть досверяет фоном, когда сеть появится.

Вторая половина офлайна это мутации без сети. Если изменение сделано офлайн, мутацию нельзя выполнить сразу - её ставят в очередь (paused) и доотправляют, когда связь вернётся. Query помечает такие мутации и при восстановлении сети проигрывает их по порядку. В паре с оптимистичным обновлением интерфейс отражает правку сразу, а фактическая отправка догоняет позже, незаметно для пользователя.

  1. Кэш персистится в localStorage или IndexedDB через persistQueryClient
  2. При заходе офлайн кэш восстанавливается из хранилища, данные видны сразу без сети
  3. Мутация, сделанная офлайн, ставится в очередь как paused, а не выполняется немедленно
  4. Сеть вернулась - очередь мутаций проигрывается по порядку, кэш сверяется с сервером фоном

Персист кэша требует осторожности с приватными данными: localStorage доступен любому скрипту на странице и переживает закрытие вкладки. Персонализированный кэш стоит чистить при выходе из аккаунта и не хранить в нём то, что не должно оставаться на устройстве.

Пользователь сделал изменение офлайн. Как ведёт себя мутация в офлайн-сценарии Query?

Связь с другими темами

Урок про паттерны поверх кэша. Дальше тема связана так:

  • TanStack Query: основы — Все паттерны опираются на useQuery, queryKey и кэш из базового урока
  • TanStack Query: мутации и optimistic — Офлайн-очереди и обновления списков строятся на мутациях и инвалидации
  • TanStack Router — Префетч на уровне маршрута через loaders поверх того же кэша, без водопадов запросов

Итог

  • Префетч заранее кладёт данные в кэш через prefetchQuery (по наведению, на уровне маршрута), и переход открывается мгновенно из готового кэша
  • Пагинация с placeholderData (бывший keepPreviousData) показывает прошлую страницу, пока грузится новая, без мигания пустым экраном
  • useInfiniteQuery ведёт страницы как один растущий кэш: getNextPageParam задаёт курсор следующей страницы для бесконечной прокрутки
  • Зависимый запрос ждёт данных предыдущего через enabled: запрос компании стартует только когда известен companyId из профиля
  • Офлайн: persistQueryClient сохраняет кэш в хранилище, чтобы показывать данные без сети, а мутации ставятся в очередь и доотправляются при возврате связи

Связанные уроки

  • sm-31-tanstack-query — Паттерны строятся поверх useQuery, queryKey и кэша из основ TanStack Query
  • sm-32-tanstack-mutations — Офлайн-очереди и оптимистичная пагинация опираются на мутации и инвалидацию из соседнего урока
  • rc-37-tanstack-router — Router вызывает префетч на уровне маршрута через loaders поверх того же кэша Query
Server-state: паттерны

0

1

Войти