Back to Ragflow

cache-placeholder-vs-initial: Understand Placeholder vs Initial Data

web/.agents/skills/tanstack-query-best-practices/rules/cache-placeholder-vs-initial.md

0.25.14.6 KB
Original Source

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

BehaviorinitialDataplaceholderData
Persisted to cacheYesNo
staleTime appliesYesNo (always fetches)
isPlaceholderDatafalsetrue
Shown to other componentsYes (cached)No
Use caseSSR, complete known dataPreview, previous page
Affects dataUpdatedAtYes (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