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
Префетч и пагинация
Префетч кладёт данные в кэш заранее, до того как они понадобятся на экране. Метод 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 помечает такие мутации и при восстановлении сети проигрывает их по порядку. В паре с оптимистичным обновлением интерфейс отражает правку сразу, а фактическая отправка догоняет позже, незаметно для пользователя.
- Кэш персистится в localStorage или IndexedDB через persistQueryClient
- При заходе офлайн кэш восстанавливается из хранилища, данные видны сразу без сети
- Мутация, сделанная офлайн, ставится в очередь как paused, а не выполняется немедленно
- Сеть вернулась - очередь мутаций проигрывается по порядку, кэш сверяется с сервером фоном
Персист кэша требует осторожности с приватными данными: 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