Files
personal-admin-2026/.agents/skills/tanstack-query-best-practices/rules/cache-placeholder-vs-initial.md
Jose Selesan 7bc3d9f898 initial commit
2026-05-28 14:33:35 -03:00

157 lines
4.6 KiB
Markdown

# cache-placeholder-vs-initial: Understand Placeholder vs Initial Data
## Priority: MEDIUM
## Explanation
`placeholderData` and `initialData` both provide data before the fetch completes, but behave differently. `initialData` is treated as real cached data, while `placeholderData` is temporary and doesn't persist to cache. Choose based on whether your fallback data should be cached.
## Bad Example
```tsx
// Using initialData when you don't want it cached
function PostPreview({ postId, previewData }: Props) {
const { data } = useQuery({
queryKey: ['posts', postId],
queryFn: () => fetchPost(postId),
initialData: previewData, // Wrong: this becomes cached "truth"
// If previewData is incomplete, it pollutes the cache
// staleTime applies to this data as if it were fetched
})
}
// Using placeholderData when you want persistence
function UserProfile({ userId }: Props) {
const { data } = useQuery({
queryKey: ['users', userId],
queryFn: () => fetchUser(userId),
placeholderData: cachedUserFromList, // Wrong: won't persist
// User navigates away and back - placeholder shown again
// No cache entry created until fetch completes
})
}
```
## Good Example: placeholderData for Temporary Display
```tsx
// Show list data while fetching detail
function PostDetail({ postId }: { postId: string }) {
const queryClient = useQueryClient()
const { data, isPlaceholderData } = useQuery({
queryKey: ['posts', postId],
queryFn: () => fetchPost(postId),
placeholderData: () => {
// Use partial data from list cache as placeholder
const posts = queryClient.getQueryData<Post[]>(['posts'])
return posts?.find(p => p.id === postId)
},
})
return (
<article className={isPlaceholderData ? 'opacity-50' : ''}>
<h1>{data?.title}</h1>
{isPlaceholderData ? (
<p>Loading full content...</p>
) : (
<div>{data?.content}</div>
)}
</article>
)
}
```
## Good Example: initialData for Known Good Data
```tsx
// SSR: Data fetched on server should be initial
function PostPage({ serverData }: { serverData: Post }) {
const { data } = useQuery({
queryKey: ['posts', serverData.id],
queryFn: () => fetchPost(serverData.id),
initialData: serverData,
// Specify when this data was fetched for proper stale calculation
initialDataUpdatedAt: serverData.fetchedAt,
})
return <PostContent post={data} />
}
// Pre-seeding cache with complete data
function App() {
const queryClient = useQueryClient()
// If you have complete, authoritative data
useEffect(() => {
queryClient.setQueryData(['config'], completeConfigData)
}, [])
}
```
## Good Example: keepPreviousData Pattern
```tsx
// Keep showing old data while fetching new (pagination, filters)
function ProductList({ page }: { page: number }) {
const { data, isPlaceholderData } = useQuery({
queryKey: ['products', page],
queryFn: () => fetchProducts(page),
placeholderData: keepPreviousData, // Built-in helper
})
return (
<div className={isPlaceholderData ? 'opacity-70' : ''}>
{data?.map(product => (
<ProductCard key={product.id} product={product} />
))}
{isPlaceholderData && <LoadingOverlay />}
</div>
)
}
```
## Comparison Table
| Behavior | `initialData` | `placeholderData` |
|----------|---------------|-------------------|
| Persisted to cache | Yes | No |
| `staleTime` applies | Yes | No (always fetches) |
| `isPlaceholderData` | `false` | `true` |
| Shown to other components | Yes (cached) | No |
| Use case | SSR, complete known data | Preview, previous page |
| Affects `dataUpdatedAt` | Yes (use `initialDataUpdatedAt`) | No |
## Good Example: Combining Both
```tsx
function PostDetail({ postId, ssrData }: Props) {
const queryClient = useQueryClient()
const { data } = useQuery({
queryKey: ['posts', postId],
queryFn: () => fetchPost(postId),
// If we have SSR data, use as initial (cached)
initialData: ssrData,
initialDataUpdatedAt: ssrData?.fetchedAt,
// If no SSR data, try to use list preview as placeholder
placeholderData: () => {
if (ssrData) return undefined // Already have initial
const posts = queryClient.getQueryData<Post[]>(['posts'])
return posts?.find(p => p.id === postId)
},
})
}
```
## Context
- `placeholderData` can be a value or function (lazy evaluation)
- `initialData` affects cache immediately on query creation
- Use `initialDataUpdatedAt` with `initialData` for proper stale calculations
- `keepPreviousData` is a built-in placeholder strategy
- Check `isPlaceholderData` to show loading indicators
- `placeholderData` is ideal for "instant" UI while fetching