Files
findyourpilot/.agents/skills/tanstack-router-best-practices/rules/nav-link-component.md
2026-03-02 21:16:26 +01:00

4.0 KiB

nav-link-component: Prefer Link Component for Navigation

Priority: MEDIUM

Explanation

Use the <Link> component for navigation instead of useNavigate() when possible. Links render proper <a> tags with valid href attributes, enabling right-click → open in new tab, better SEO, and accessibility.

Bad Example

// Using onClick with navigate - loses standard link behavior
function PostCard({ post }: { post: Post }) {
  const navigate = useNavigate()

  return (
    <div
      onClick={() => navigate({ to: '/posts/$postId', params: { postId: post.id } })}
      className="post-card"
    >
      <h2>{post.title}</h2>
      <p>{post.excerpt}</p>
    </div>
  )
}
// Problems:
// - No right-click → open in new tab
// - No cmd/ctrl+click for new tab
// - Not announced as link to screen readers
// - No valid href for SEO

Good Example

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

function PostCard({ post }: { post: Post }) {
  return (
    <Link
      to="/posts/$postId"
      params={{ postId: post.id }}
      className="post-card"
    >
      <h2>{post.title}</h2>
      <p>{post.excerpt}</p>
    </Link>
  )
}
// Benefits:
// - Renders <a href="/posts/123">
// - Right-click menu works
// - Cmd/Ctrl+click opens new tab
// - Screen readers announce as link
// - Preloading works on hover

Good Example: With Search Params

function FilteredLink() {
  return (
    <Link
      to="/products"
      search={{ category: 'electronics', sort: 'price' }}
    >
      View Electronics
    </Link>
  )
}

// Preserving existing search params
function SortLink({ sort }: { sort: 'asc' | 'desc' }) {
  return (
    <Link
      to="."  // Current route
      search={(prev) => ({ ...prev, sort })}
    >
      Sort {sort === 'asc' ? 'Ascending' : 'Descending'}
    </Link>
  )
}

Good Example: With Active States

function NavLink({ to, children }: { to: string; children: React.ReactNode }) {
  return (
    <Link
      to={to}
      activeProps={{
        className: 'nav-link-active',
        'aria-current': 'page',
      }}
      inactiveProps={{
        className: 'nav-link',
      }}
      activeOptions={{
        exact: true,  // Only active on exact match
      }}
    >
      {children}
    </Link>
  )
}

// Or use render props for more control
function CustomNavLink({ to, children }: { to: string; children: React.ReactNode }) {
  return (
    <Link to={to}>
      {({ isActive }) => (
        <span className={isActive ? 'text-blue-600 font-bold' : 'text-gray-600'}>
          {children}
          {isActive && <CheckIcon className="ml-2" />}
        </span>
      )}
    </Link>
  )
}

Good Example: With Preloading

function PostList({ posts }: { posts: Post[] }) {
  return (
    <ul>
      {posts.map(post => (
        <li key={post.id}>
          <Link
            to="/posts/$postId"
            params={{ postId: post.id }}
            preload="intent"      // Preload on hover/focus
            preloadDelay={100}    // Wait 100ms before preloading
          >
            {post.title}
          </Link>
        </li>
      ))}
    </ul>
  )
}

When to Use useNavigate Instead

// 1. After form submission
const createPost = useMutation({
  mutationFn: submitPost,
  onSuccess: (data) => {
    navigate({ to: '/posts/$postId', params: { postId: data.id } })
  },
})

// 2. After authentication
async function handleLogin(credentials: Credentials) {
  await login(credentials)
  navigate({ to: '/dashboard' })
}

// 3. Programmatic redirects
useEffect(() => {
  if (!isAuthenticated) {
    navigate({ to: '/login', search: { redirect: location.pathname } })
  }
}, [isAuthenticated])

Context

  • <Link> renders actual <a> tags with proper href
  • Supports all standard link behaviors (middle-click, cmd+click, etc.)
  • Enables preloading on hover/focus
  • Better for SEO - crawlers can follow links
  • Reserve useNavigate for side effects and programmatic navigation
  • Use <Navigate> component for immediate redirects on render