Placeholder data allows you to display temporary content while a query is fetching. Unlike initialData, placeholder data is not persisted to the cache and doesn’t affect the query’s state.
Basic Usage
Provide placeholder data using the placeholderData option:
import { useQuery } from '@tanstack/react-query'
function Todo({ id }) {
const { data, isPlaceholderData } = useQuery({
queryKey: ['todo', id],
queryFn: () => fetchTodo(id),
placeholderData: {
id,
title: 'Loading...',
completed: false
}
})
return (
<div>
<h1>{data.title}</h1>
{isPlaceholderData && <span className="loading">Loading...</span>}
</div>
)
}
The isPlaceholderData flag tells you when you’re showing placeholder data vs. real data.
Keep Previous Data
A common pattern is keeping the previous query’s data while fetching new data. Use the keepPreviousData helper:
import { useQuery, keepPreviousData } from '@tanstack/react-query'
function Todos({ page }) {
const { data, isPlaceholderData } = useQuery({
queryKey: ['todos', page],
queryFn: () => fetchTodos(page),
placeholderData: keepPreviousData
})
return (
<div>
<ul>
{data.items.map(todo => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
{isPlaceholderData && <div>Loading new page...</div>}
<button onClick={() => setPage(p => p - 1)}>Previous</button>
<button onClick={() => setPage(p => p + 1)}>Next</button>
</div>
)
}
The keepPreviousData function returns the previous data when available, preventing content from disappearing during pagination or filtering.
Placeholder Data Function
Compute placeholder data dynamically using a function:
import { useQuery, useQueryClient } from '@tanstack/react-query'
function TodoDetail({ id }) {
const queryClient = useQueryClient()
const { data, isPlaceholderData } = useQuery({
queryKey: ['todo', id],
queryFn: () => fetchTodo(id),
placeholderData: () => {
// Find this todo in the cached 'todos' list
const todos = queryClient.getQueryData(['todos'])
return todos?.find(todo => todo.id === id)
}
})
return (
<div>
<h1>{data?.title}</h1>
{isPlaceholderData && <p>Showing cached preview...</p>}
</div>
)
}
Placeholder data works correctly with select transforms:
import { useQuery, keepPreviousData } from '@tanstack/react-query'
function TodoCount({ filter }) {
const { data, isPlaceholderData } = useQuery({
queryKey: ['todos', filter],
queryFn: () => fetchTodos(filter),
select: (todos) => todos.length,
placeholderData: keepPreviousData
})
return (
<div>
<p>Count: {data}</p>
{isPlaceholderData && <span>Updating...</span>}
</div>
)
}
Previous data is kept
The previous query result is used as placeholder data
Select function runs
The select transform runs on the placeholder data
New data replaces placeholder
When the fetch completes, real data replaces the placeholder
Query State Behavior
Placeholder data affects the query state differently than initial data:
function Example() {
const { data, status, fetchStatus, isPlaceholderData } = useQuery({
queryKey: ['data'],
queryFn: fetchData,
placeholderData: { value: 'placeholder' }
})
// While loading with placeholder:
// status: 'pending'
// fetchStatus: 'fetching'
// isPlaceholderData: true
// data: { value: 'placeholder' }
// After fetch completes:
// status: 'success'
// fetchStatus: 'idle'
// isPlaceholderData: false
// data: { value: 'real data' }
}
Placeholder data does not change the query status to success. The query remains in pending state until real data is fetched.
A complete pagination example using keepPreviousData:
import { useQuery, keepPreviousData } from '@tanstack/react-query'
import { useState } from 'react'
function PaginatedTodos() {
const [page, setPage] = useState(0)
const { data, isPlaceholderData, isPending } = useQuery({
queryKey: ['todos', page],
queryFn: async () => {
const res = await fetch(`/api/todos?page=${page}`)
return res.json()
},
placeholderData: keepPreviousData,
staleTime: 5000
})
return (
<div>
{isPending && !isPlaceholderData ? (
<div>Loading first page...</div>
) : (
<>
<ul>
{data.items.map(todo => (
<li key={todo.id}>
{todo.title}
</li>
))}
</ul>
<div>
<button
onClick={() => setPage(old => Math.max(0, old - 1))}
disabled={page === 0}
>
Previous
</button>
<span>Page {page + 1}</span>
<button
onClick={() => setPage(old => old + 1)}
disabled={isPlaceholderData || !data.hasMore}
>
Next
</button>
</div>
{isPlaceholderData && (
<div className="loading-overlay">
Loading next page...
</div>
)}
</>
)}
</div>
)
}
TypeScript
Placeholder data must match the query’s data type:
interface Todo {
id: number
title: string
completed: boolean
}
function useTodo(id: number) {
return useQuery({
queryKey: ['todo', id],
queryFn: async (): Promise<Todo> => {
const res = await fetch(`/api/todos/${id}`)
return res.json()
},
// ✅ Correct: matches Todo type
placeholderData: { id, title: 'Loading...', completed: false },
// ❌ Error: incompatible type
// placeholderData: 'loading'
})
}
When to Use Placeholder Data
Use placeholderData when:
- You want to keep previous results during pagination
- You’re implementing optimistic UI updates
- You want to show a skeleton or loading state with partial data
- You need temporary data that shouldn’t be cached
For pre-populating with real data, use initialData instead.
keepPreviousData Implementation
The keepPreviousData helper is a simple function that returns the previous data:
export function keepPreviousData<T>(
previousData: T | undefined,
): T | undefined {
return previousData
}
Source: packages/query-core/src/utils.ts:407
Differences from Initial Data
| Feature | placeholderData | initialData |
|---|
| Persisted to cache | ❌ No | ✅ Yes |
| Changes query status | ❌ No | ✅ Yes (to success) |
| Triggers callbacks | ❌ No | ✅ Yes |
| Temporary | ✅ Yes | ❌ No |
| Use case | Loading states, pagination | SSR, pre-populated data |
See Also