skip to content

In TanStack Query v5, how do placeholderData and initialData differ in what reaches the cache and how staleTime treats them?

level: middleimportance: should knowfreq 58%

answer

  1. one persists, one pretends
  2. which one other readers see
  3. success status in both cases
  4. a flag marks the stand-in
  5. the seed's age can be supplied

basics

~10 s

initialData is written to the cache as real data and ages under staleTime as if just fetched; placeholderData is shown only to that observer while the query has no data, and is never cached.

solid answer

~40 s

`initialData` seeds the cache: it applies only when the key has no entry yet, is stored like fetched data, and every component reading that key sees it. Its timestamp is the current time unless you pass `initialDataUpdatedAt`, so with the default `staleTime: 0` it is stale at once and the query refetches on mount, while a longer `staleTime` can suppress that fetch. `placeholderData` is per observer and never persisted: the query stays empty and fetches as usual, but the hook returns the placeholder with `status: 'success'` and `isPlaceholderData: true` until real data arrives. Rule of thumb: complete, trustworthy data goes in `initialData`; partial or fake data, such as a list preview, goes in `placeholderData`.

code

tsx · 15 lines
tsx
import { useQuery, useQueryClient } from '@tanstack/react-query'

type Post = { id: number; title: string; body?: string }

declare function fetchPost(id: number): Promise<Post>

export function usePost(id: number) {
  const queryClient = useQueryClient()
  return useQuery({
    queryKey: ['posts', id],
    queryFn: () => fetchPost(id),
    placeholderData: () =>
      queryClient.getQueryData<Post[]>(['posts'])?.find((p) => p.id === id),
  })
}

go deeper

for a junior

Recall that initialData goes into the cache while placeholderData is only shown, and that both make the hook report success instead of a loading state.

for a middle

Explain how initialData's timestamp meets staleTime, why initialDataUpdatedAt exists, and what isPlaceholderData tells the component.

for a senior

Show you choose by data completeness, avoid persisting previews that other screens then trust, and seed detail queries with the source list's real age.

for a principal

Discuss conventions for seeding caches across teams so partial objects never leak into shared keys, and how that interacts with server-rendered data.

## Two ways to render before the first fetch returns A `useQuery` call with no cached data normally starts in a hard loading state. TanStack Query v5 offers two options that let a component show something useful at once, and they look alike from the component's point of view: in both cases `data` is defined and `status` is `'success'`. The difference is **where the value lives** and **what the rest of the library believes about it**. - **`initialData`** is real data. It is written into the query cache as if a fetch had just returned it. - **`placeholderData`** is a stand-in. It is handed to one observer while the query itself stays empty, and it is never written to the cache. ## initialData: seeding the cache - It only applies when the query is **created**, meaning the key has no cache entry yet. If the key is already cached, the option is ignored and the cached data wins. - It is **persisted**. `queryClient.getQueryData` returns it, every other component on the same key sees it, and it lives until the entry is garbage collected or replaced by a fetch. - Its timestamp, `dataUpdatedAt`, is `initialDataUpdatedAt` if you pass one, otherwise the current time. The library treats it **as if it had just been fetched**. - A function form runs **once**, when the query is created, which suits an expensive computation. The `staleTime` interplay follows directly from that timestamp: 1. With the default `staleTime: 0`, the seeded data is stale immediately, so the component renders it and `refetchOnMount` starts a background fetch. 2. With `staleTime: 60_000`, the seeded data counts as fresh for a minute and no fetch happens on mount. 3. With `initialDataUpdatedAt` set to the data's real age, the library compares that age against `staleTime` and refetches only if the seed is older than the window. Because `initialData` is persisted and trusted, never put **partial** data there. A detail query seeded with a list row's preview would store the preview as the full object, and with any `staleTime` it could serve that preview as complete data. ## placeholderData: a stand-in for one observer - The query's own state stays **pending with no data**, so the fetch runs as usual on mount. - The observer's result reports `status: 'success'`, `data` set to the placeholder, and **`isPlaceholderData: true`**; `dataUpdatedAt` stays `0`. - It is used only while the query has no data. Once the fetch resolves, real data replaces it and the flag turns `false`. - It can be a value, or a function receiving the previous data and previous query that this observer showed. Memoize a computed value, or use the function form, so it is not rebuilt on every render. - Placeholder data goes through `select` like real data. ## Side by side | Aspect | `initialData` | `placeholderData` | |---|---|---| | Written to the query cache | yes | no | | Visible to other components on the key | yes | no, only this observer | | Measured against `staleTime` | yes, from `dataUpdatedAt` | no; the query has no data, so it fetches | | `status` while shown | `'success'` | `'success'` | | Flag on the result | `isPlaceholderData: false` | `isPlaceholderData: true` | | Applies when | the key has no cache entry yet | the query is pending with no data | | Suitable data | complete and trustworthy | partial, preview or fake | ## Choosing between them - The value is the **complete** object you would get from the server, for example a detail item taken from a cached list whose rows are full objects: use `initialData`, and pass the list's age through `initialDataUpdatedAt`, for example `queryClient.getQueryState(['todos'])?.dataUpdatedAt`. - The value is a **preview** or a skeleton, such as a title and snippet from a list: use `placeholderData`, and let the UI dim or badge it while `isPlaceholderData` is `true`. - You want the query to decide freshness from real timestamps: `initialData` with `initialDataUpdatedAt`, never `initialData` with an arbitrary long `staleTime`. ## Traps - Expecting `initialData` to overwrite an existing cache entry. It does not; only a fetch or a manual cache write does. - Seeding old data with `initialData` and a `staleTime`, but no `initialDataUpdatedAt`: the old data is treated as fresh from the moment of seeding. - Acting on placeholder data, for example submitting a form built from it. Check `isPlaceholderData` first.

  • You pass initialData to a TanStack Query v5 query whose key is already cached. What does the hook return?
    The cached data. `initialData` only seeds a query that does not exist yet; once the key has an entry, whether from a fetch, a manual cache write or an earlier seed, the option is ignored. Freshness is then judged from that entry's own `dataUpdatedAt` against `staleTime`.
  • How do you seed a detail query from a cached list without treating old data as fresh?
    Pass `initialData` as a function that finds the item in `queryClient.getQueryData` for the list key, and `initialDataUpdatedAt` as a function returning the list's `queryClient.getQueryState(...)?.dataUpdatedAt`. The detail query inherits the list's age, so `staleTime` decides correctly whether to refetch on mount. Do this only when list rows are complete objects.

saying these in an interview costs you the question

  • placeholderData is saved to the cache like initialData, just marked as temporary.
  • initialData is always stale, so it can never prevent a fetch on mount.
  • A query showing placeholderData reports status pending until the fetch finishes.
  • initialData overwrites whatever the cache already holds for the key.
  • Partial preview data is safe in initialData because a refetch will soon replace it.