PlaceholderData и initialData

Опция initialData позволяет заранее поместить данные в кэш TanStack Query ещё до выполнения запроса. Хук useQuery сразу получает готовое значение и переходит в состояние успешной загрузки (success), даже если сетевой запрос ещё не был выполнен.

Это особенно важно в нескольких сценариях:

  • серверный рендеринг;
  • гидратация состояния;
  • предварительная загрузка данных;
  • отображение данных из локального состояния;
  • использование уже существующих данных как стартового значения;
  • устранение пустых состояний интерфейса.

Пример базового использования:

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

function Users() {
  const query = useQuery({
    queryKey: ['users'],
    queryFn: fetchUsers,
    initialData: [
      { id: 1, name: 'Alex' },
      { id: 2, name: 'John' }
    ]
  })

  return (
    <ul>
      {query.data.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  )
}

При первом рендере список уже будет содержать данные из initialData.


Как работает initialData

Когда TanStack Query видит initialData, происходит следующее:

  1. Данные записываются в кэш.
  2. Query сразу получает статус success.
  3. data становится доступным синхронно.
  4. Затем может быть выполнен фоновый запрос.

Главная особенность заключается в том, что initialData считается полноценными данными кэша.

Это означает:

  • данные сохраняются в Query Cache;
  • ими могут пользоваться другие компоненты;
  • данные участвуют в механизмах staleTime;
  • они имеют timestamp последнего обновления.

Поведение без initialData

Стандартный запрос проходит несколько стадий:

const query = useQuery({
  queryKey: ['posts'],
  queryFn: fetchPosts
})

Сначала:

status === 'pending'
isLoading === true
data === undefined

После завершения запроса:

status === 'success'
isLoading === false
data !== undefined

Поведение с initialData

Теперь добавим стартовые данные:

const query = useQuery({
  queryKey: ['posts'],
  queryFn: fetchPosts,
  initialData: []
})

Сразу после рендера:

status === 'success'
isLoading === false
data === []

При этом TanStack Query всё ещё может отправить сетевой запрос для актуализации данных.


initialData и фоновый refetch

По умолчанию данные из initialData считаются устаревшими (stale).

Из-за этого после монтирования компонента TanStack Query обычно запускает refetch.

Пример:

const query = useQuery({
  queryKey: ['profile'],
  queryFn: fetchProfile,
  initialData: {
    id: 1,
    name: 'Unknown'
  }
})

Последовательность будет такой:

  1. Компонент получает Unknown.
  2. Query переходит в success.
  3. Выполняется HTTP-запрос.
  4. Данные обновляются реальным ответом сервера.

Управление свежестью через staleTime

Часто стартовые данные считаются актуальными некоторое время.

Для этого используется staleTime.

const query = useQuery({
  queryKey: ['settings'],
  queryFn: fetchSettings,
  initialData: defaultSettings,
  staleTime: 1000 * 60 * 5
})

Теперь:

  • данные будут считаться свежими 5 минут;
  • автоматический refetch после монтирования не выполнится;
  • интерфейс не создаст лишний сетевой запрос.

Функция в initialData

initialData может быть функцией.

Это полезно, если вычисление дорогое.

const query = useQuery({
  queryKey: ['products'],
  queryFn: fetchProducts,
  initialData: () => {
    return generateLargeDataset()
  }
})

Функция вызывается только один раз при инициализации Query.


Использование данных другого Query

Очень распространённый сценарий — получение данных детали объекта из уже загруженного списка.

Например, имеется список пользователей:

const usersQuery = useQuery({
  queryKey: ['users'],
  queryFn: fetchUsers
})

Далее нужен запрос конкретного пользователя:

const userQuery = useQuery({
  queryKey: ['user', userId],
  queryFn: () => fetchUser(userId),
  initialData: () => {
    return queryClient
      .getQueryData(['users'])
      ?.find(user => user.id === userId)
  }
})

Преимущества:

  • мгновенный рендер;
  • отсутствие пустого состояния;
  • плавный UX;
  • минимизация визуальных скачков интерфейса.

initialDataUpdatedAt

Иногда важно указать время актуальности данных вручную.

Для этого существует initialDataUpdatedAt.

const query = useQuery({
  queryKey: ['news'],
  queryFn: fetchNews,
  initialData: cachedNews,
  initialDataUpdatedAt: Date.now()
})

Теперь TanStack Query понимает, что данные были обновлены только что.


Почему это важно

Без initialDataUpdatedAt библиотека считает данные потенциально устаревшими.

В результате может произойти немедленный refetch.

С initialDataUpdatedAt можно:

  • синхронизировать время обновления;
  • избегать ненужных запросов;
  • корректно восстанавливать кэш;
  • улучшать SSR и hydration.

Разница между initialData и placeholderData

Эти опции часто путают.

Различие принципиальное.

initialData

  • попадает в кэш;
  • считается настоящими данными;
  • влияет на состояние Query;
  • сохраняется между компонентами.

placeholderData

  • не записывается в кэш;
  • существует только временно;
  • используется как визуальная заглушка;
  • заменяется реальными данными после запроса.

Назначение placeholderData

placeholderData используется для временного отображения структуры данных, пока настоящий запрос ещё выполняется.

Это механизм визуального заполнения интерфейса.

Пример:

const query = useQuery({
  queryKey: ['articles'],
  queryFn: fetchArticles,
  placeholderData: []
})

Интерфейс получает:

data === []

Но эти данные не считаются настоящими.


Главное отличие от initialData

С placeholderData Query остаётся в состоянии загрузки.

isPlaceholderData === true

Это специальный флаг TanStack Query.


Проверка isPlaceholderData

const query = useQuery({
  queryKey: ['products'],
  queryFn: fetchProducts,
  placeholderData: []
})

if (query.isPlaceholderData) {
  console.log('Показываются временные данные')
}

Как работает placeholderData

Последовательность:

  1. Query стартует.
  2. Отображаются placeholder-данные.
  3. Выполняется сетевой запрос.
  4. Приходит настоящий ответ.
  5. Placeholder исчезает.

Пример с пагинацией

Это один из самых популярных сценариев использования.

const query = useQuery({
  queryKey: ['posts', page],
  queryFn: () => fetchPosts(page),
  placeholderData: previousData
})

Пока новая страница загружается:

  • старая страница остаётся на экране;
  • интерфейс не мерцает;
  • пользователь не видит пустое состояние.

Использование keepPreviousData

Ранее для подобных задач активно использовалась опция keepPreviousData.

В новых версиях TanStack Query чаще применяется placeholderData.

Пример:

placeholderData: previousData => previousData

Placeholder как функция

placeholderData может получать предыдущие данные.

const query = useQuery({
  queryKey: ['users', page],
  queryFn: () => fetchUsers(page),
  placeholderData: previousData => previousData
})

Это создаёт эффект непрерывного интерфейса.


Временные данные и состояние загрузки

Даже если placeholder отображается, запрос всё ещё считается загружаемым.

isFetching === true

Это важно для:

  • индикаторов обновления;
  • спиннеров;
  • skeleton-компонентов;
  • отображения фоновой загрузки.

Использование Skeleton UI вместе с placeholderData

if (query.isLoading && !query.data) {
  return <Skeleton />
}

После появления placeholder:

query.data

уже существует, поэтому интерфейс может отобразить старые данные вместо пустого экрана.


placeholderData не сохраняется в кэше

Это фундаментальное свойство.

После завершения запроса:

  • placeholder полностью исчезает;
  • в кэше остаются только реальные данные;
  • другие компоненты не увидят placeholder.

Когда использовать initialData

initialData подходит, если:

  • данные действительно существуют;
  • имеется SSR;
  • есть hydrated state;
  • используется local cache;
  • данные получены заранее;
  • требуется мгновенный success-state.

Когда использовать placeholderData

placeholderData подходит, если:

  • нужен плавный UX;
  • требуется убрать мерцание;
  • отображаются старые данные при пагинации;
  • нужны временные данные;
  • данные не должны попадать в кэш.

Сравнение поведения

Особенность initialData placeholderData
Записывается в кэш Да Нет
Считается настоящими данными Да Нет
Query получает success Да Нет
Может быть stale Да Нет
Используется как временный UI Частично Да
Подходит для SSR Да Нет
Подходит для пагинации Иногда Да

SSR и initialData

Во время серверного рендеринга initialData особенно полезен.

Сервер может заранее получить данные:

const dehydratedState = dehydrate(queryClient)

После этого клиент получает готовый кэш.

Преимущества:

  • отсутствие повторного loading;
  • быстрый First Paint;
  • улучшение SEO;
  • минимизация запросов.

Гидратация состояния

TanStack Query поддерживает:

  • dehydrate;
  • hydrate.

При гидратации данные становятся аналогом initialData, но уже на уровне всего Query Cache.


Частая ошибка с placeholderData

Некоторые разработчики ожидают, что placeholder сохранится в кэше.

Например:

placeholderData: []

Но после размонтирования компонента эти данные исчезают полностью.


Частая ошибка с initialData

Ошибка возникает, когда initialData используется как фейковые данные.

initialData: []

Если сервер должен вернуть список позже, Query уже считается успешным.

Из-за этого:

  • пропадает состояние loading;
  • сложнее отображать реальную загрузку;
  • логика интерфейса становится неоднозначной.

Комбинация с enabled

const query = useQuery({
  queryKey: ['profile', userId],
  queryFn: () => fetchProfile(userId),
  enabled: !!userId,
  placeholderData: previousData => previousData
})

Даже при изменении userId старые данные сохранятся до завершения нового запроса.


Влияние на UX

Правильное использование initialData и placeholderData позволяет:

  • устранять скачки интерфейса;
  • избегать пустых экранов;
  • уменьшать визуальный шум;
  • ускорять отображение;
  • создавать ощущение мгновенной загрузки;
  • скрывать сетевые задержки.

Архитектурное различие

initialData

Источник истины.

Данные считаются реальными.

placeholderData

Временная визуальная прослойка.

Данные существуют только до завершения запроса.


Практический пример сравнения

initialData

useQuery({
  queryKey: ['user'],
  queryFn: fetchUser,
  initialData: cachedUser
})

Поведение:

  • success сразу;
  • данные в кэше;
  • другие компоненты получают эти данные.

placeholderData

useQuery({
  queryKey: ['user'],
  queryFn: fetchUser,
  placeholderData: cachedUser
})

Поведение:

  • временное отображение;
  • кэш не изменяется;
  • данные исчезнут после ответа сервера.