Back to Ragflow

pf-intent-prefetch: Prefetch on User Intent (Hover, Focus)

web/.agents/skills/tanstack-query-best-practices/rules/pf-intent-prefetch.md

0.25.13.2 KB
Original Source

pf-intent-prefetch: Prefetch on User Intent (Hover, Focus)

Priority: MEDIUM

Explanation

Prefetch data when users show intent to navigate (hover, focus) rather than waiting for click. This eliminates perceived loading time for likely next actions.

Bad Example

tsx
// No prefetching - data fetches on click
function PostList({ posts }: { posts: Post[] }) {
  return (
    <ul>
      {posts.map(post => (
        <li key={post.id}>
          <Link to={`/posts/${post.id}`}>
            {post.title}
          </Link>
        </li>
      ))}
    </ul>
  )
}

Good Example

tsx
import { useQueryClient } from '@tanstack/react-query'
import { postQueries } from '@/lib/queries'

function PostList({ posts }: { posts: Post[] }) {
  const queryClient = useQueryClient()

  const handlePrefetch = (postId: number) => {
    queryClient.prefetchQuery({
      ...postQueries.detail(postId),
      staleTime: 60 * 1000,  // Consider fresh for 1 minute
    })
  }

  return (
    <ul>
      {posts.map(post => (
        <li key={post.id}>
          <Link
            to={`/posts/${post.id}`}
            onMouseEnter={() => handlePrefetch(post.id)}
            onFocus={() => handlePrefetch(post.id)}
          >
            {post.title}
          </Link>
        </li>
      ))}
    </ul>
  )
}

Good Example: With TanStack Router

tsx
import { Link } from '@tanstack/react-router'

// TanStack Router has built-in prefetching
function PostList({ posts }: { posts: Post[] }) {
  return (
    <ul>
      {posts.map(post => (
        <li key={post.id}>
          <Link
            to="/posts/$postId"
            params={{ postId: post.id }}
            preload="intent"  // Prefetch on hover/focus
          >
            {post.title}
          </Link>
        </li>
      ))}
    </ul>
  )
}

// Or set as router default
const router = createRouter({
  routeTree,
  defaultPreload: 'intent',
  defaultPreloadDelay: 100,  // Wait 100ms before prefetching
})

Good Example: Prefetch with Delay

tsx
function PostLink({ post }: { post: Post }) {
  const queryClient = useQueryClient()
  const timeoutRef = useRef<NodeJS.Timeout>()

  const handleMouseEnter = () => {
    // Delay prefetch to avoid unnecessary requests on quick mouse movements
    timeoutRef.current = setTimeout(() => {
      queryClient.prefetchQuery(postQueries.detail(post.id))
    }, 100)
  }

  const handleMouseLeave = () => {
    if (timeoutRef.current) {
      clearTimeout(timeoutRef.current)
    }
  }

  return (
    <Link
      to={`/posts/${post.id}`}
      onMouseEnter={handleMouseEnter}
      onMouseLeave={handleMouseLeave}
    >
      {post.title}
    </Link>
  )
}

Prefetch Triggers

TriggerWhen to Use
onMouseEnterDesktop, links/buttons user will likely click
onFocusKeyboard navigation, accessibility
onTouchStartMobile, before navigation
Component mountLikely next pages, wizard steps
Intersection ObserverBelow-fold content

Context

  • Set appropriate staleTime when prefetching to avoid immediate refetch
  • Consider mobile where hover isn't available
  • Don't prefetch everything - focus on likely paths
  • Prefetched data uses gcTime for retention
  • Watch network tab to verify prefetch timing