initial commit
This commit is contained in:
@@ -0,0 +1,156 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user