Files
Jose Selesan 7bc3d9f898 initial commit
2026-05-28 14:33:35 -03:00

4.6 KiB

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

// 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

// 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

// 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

// 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

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