144 lines
3.3 KiB
Markdown
144 lines
3.3 KiB
Markdown
# 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>
|
|
{/* User clicks, waits for data to load */}
|
|
</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
|
|
|
|
| Trigger | When to Use |
|
|
|---------|-------------|
|
|
| `onMouseEnter` | Desktop, links/buttons user will likely click |
|
|
| `onFocus` | Keyboard navigation, accessibility |
|
|
| `onTouchStart` | Mobile, before navigation |
|
|
| Component mount | Likely next pages, wizard steps |
|
|
| Intersection Observer | Below-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
|