Link

<Link> 是一個擴展 HTML <a> 元素的 React 元件,提供 預取 (prefetching) 和路由間的客戶端導航功能。它是 Next.js 中在路由之間導航的主要方式。

基本用法:

import Link from 'next/link'

export default function Page() {
  return <Link href="/dashboard">Dashboard</Link>
}

參考

以下屬性可以傳遞給 <Link> 元件:

屬性範例類型必填
hrefhref="/dashboard"字串或物件
replacereplace={false}布林值-
scrollscroll={false}布林值-
prefetchprefetch={false}布林值或 null-
onNavigateonNavigate={(e) => {}}函式-

須知:可以將 <a> 標籤屬性(如 classNametarget="_blank")作為屬性添加到 <Link>,這些屬性將傳遞給底層的 <a> 元素。

href (必填)

要導航到的路徑或 URL。

import Link from 'next/link'

// 導航到 /about?name=test
export default function Page() {
  return (
    <Link
      href={{
        pathname: '/about',
        query: { name: 'test' },
      }}
    >
      About
    </Link>
  )
}

replace

預設為 false 當設為 true 時,next/link 會替換當前的歷史狀態,而不是在 瀏覽器的歷史記錄 堆疊中添加一個新的 URL。

import Link from 'next/link'

export default function Page() {
  return (
    <Link href="/dashboard" replace>
      Dashboard
    </Link>
  )
}

scroll

預設為 true Next.js 中 <Link> 的預設滾動行為是 保持滾動位置,類似於瀏覽器處理前進和後退導航的方式。當您導航到新的 頁面 (Page) 時,只要該頁面在視口中可見,滾動位置將保持不變。但是,如果頁面不在視口中,Next.js 會滾動到第一個頁面元素的頂部。

scroll = {false} 時,Next.js 不會嘗試滾動到第一個頁面元素。

須知:Next.js 在管理滾動行為之前會檢查 scroll: false。如果滾動已啟用,它會識別導航的相關 DOM 節點並檢查每個頂層元素。所有不可滾動的元素和沒有渲染 HTML 的元素都會被跳過,這包括固定或絕對定位的元素,以及使用 getBoundingClientRect 計算的非可見元素。Next.js 然後繼續檢查兄弟元素,直到識別出視口中可見的可滾動元素。

import Link from 'next/link'

export default function Page() {
  return (
    <Link href="/dashboard" scroll={false}>
      Dashboard
    </Link>
  )
}

prefetch

<Link /> 元件進入用戶的視口(初始或通過滾動)時,預取會發生。Next.js 會在後台預取並加載連結的路由(由 href 表示)及其數據,以提高客戶端導航的性能。如果用戶懸停在 <Link /> 上時預取的數據已過期,Next.js 將嘗試再次預取。預取僅在生產環境中啟用

可以傳遞給 prefetch 屬性的值如下:

  • null (預設):預取行為取決於路由是靜態還是動態。對於靜態路由,將預取完整路由(包括所有數據)。對於動態路由,將預取到最近的 loading.js 邊界的部分路由。
  • true:對於靜態和動態路由,都會預取完整路由。
  • false:無論是進入視口還是懸停,都不會發生預取。
import Link from 'next/link'

export default function Page() {
  return (
    <Link href="/dashboard" prefetch={false}>
      Dashboard
    </Link>
  )
}

onNavigate

在客戶端導航期間調用的事件處理程序。該處理程序接收一個事件對象,其中包括 preventDefault() 方法,允許您在需要時取消導航。

import Link from 'next/link'

export default function Page() {
  return (
    <Link
      href="/dashboard"
      onNavigate={(e) => {
        // 僅在 SPA 導航期間執行
        console.log('導航中...')

        // 可選阻止導航
        // e.preventDefault()
      }}
    >
      Dashboard
    </Link>
  )
}

須知:雖然 onClickonNavigate 看起來相似,但它們有不同的用途。onClick 執行所有點擊事件,而 onNavigate 僅在客戶端導航期間運行。一些關鍵區別:

  • 當使用修飾鍵(Ctrl/Cmd + 點擊)時,onClick 會執行但 onNavigate 不會,因為 Next.js 會阻止新標籤的預設導航。
  • 外部 URL 不會觸發 onNavigate,因為它僅用於客戶端和同源導航。
  • 帶有 download 屬性的連結會與 onClick 一起工作,但不會與 onNavigate 一起工作,因為瀏覽器會將連結的 URL 視為下載。

範例

以下範例演示瞭如何在不同的場景中使用 <Link> 元件。

連結到動態區段

當連結到 動態區段 (dynamic segments) 時,您可以使用 模板字面量和插值 (template literals and interpolation) 來生成連結列表。例如,生成部落格文章列表:

import Link from 'next/link'

interface Post {
  id: number
  title: string
  slug: string
}

export default function PostList({ posts }: { posts: Post[] }) {
  return (
    <ul>
      {posts.map((post) => (
        <li key={post.id}>
          <Link href={`/blog/${post.slug}`}>{post.title}</Link>
        </li>
      ))}
    </ul>
  )
}

檢查當前活動連結

您可以使用 usePathname() 來判斷連結是否處於活動狀態。例如,要為當前活動連結添加 class,您可以檢查當前 pathname 是否與連結的 href 匹配:

'use client'

import { usePathname } from 'next/navigation'
import Link from 'next/link'

export function Links() {
  const pathname = usePathname()

  return (
    <nav>
      <Link className={`link ${pathname === '/' ? 'active' : ''}`} href="/">
        Home
      </Link>

      <Link
        className={`link ${pathname === '/about' ? 'active' : ''}`}
        href="/about"
      >
        About
      </Link>
    </nav>
  )
}

滾動至特定 id

如果您想在導航時滾動至特定 id,可以在 URL 後附加 # 雜湊連結,或直接將雜湊連結傳遞給 href 屬性。這是可行的,因為 <Link> 最終會渲染為 <a> 元素。

<Link href="/dashboard#settings">Settings</Link>

// 輸出
<a href="/dashboard#settings">Settings</a>

須知

  • 如果導航時 Page 不在視窗可見範圍內,Next.js 會自動滾動至該頁面。

連結至動態路由區段

對於 動態路由區段 (dynamic route segments),使用模板字面值來建立連結路徑會非常方便。

例如,您可以生成一個連結列表,指向動態路由 app/blog/[slug]/page.js

import Link from 'next/link'

export default function Page({ posts }) {
  return (
    <ul>
      {posts.map((post) => (
        <li key={post.id}>
          <Link href={`/blog/${post.slug}`}>{post.title}</Link>
        </li>
      ))}
    </ul>
  )
}

當子元件是包裝 <a> 標籤的自訂元件時

如果 Link 的子元件是一個包裝 <a> 標籤的自訂元件,您必須在 Link 上添加 passHref。當您使用像 styled-components 這樣的函式庫時,這點尤其重要。如果不這樣做,<a> 標籤將不會有 href 屬性,這會影響網站的無障礙性和 SEO。如果您使用 ESLint,內建的 next/link-passhref 規則可以確保正確使用 passHref

import Link from 'next/link'
import styled from 'styled-components'

// 這會建立一個包裝 <a> 標籤的自訂元件
const RedLink = styled.a`
  color: red;
`

function NavLink({ href, name }) {
  return (
    <Link href={href} passHref legacyBehavior>
      <RedLink>{name}</RedLink>
    </Link>
  )
}

export default NavLink
  • 如果您使用 emotion 的 JSX pragma 功能 (@jsx jsx),即使直接使用 <a> 標籤,也必須使用 passHref
  • 元件應支援 onClick 屬性以正確觸發導航。

嵌套函式元件

如果 Link 的子元件是一個函式元件,除了使用 passHreflegacyBehavior 外,您還必須使用 React.forwardRef 包裝該元件:

import Link from 'next/link'
import React from 'react'

// 為 MyButton 定義 props 類型
interface MyButtonProps {
  onClick?: React.MouseEventHandler<HTMLAnchorElement>
  href?: string
}

// 使用 React.ForwardRefRenderFunction 正確類型化轉發的 ref
const MyButton: React.ForwardRefRenderFunction<
  HTMLAnchorElement,
  MyButtonProps
> = ({ onClick, href }, ref) => {
  return (
    <a href={href} onClick={onClick} ref={ref}>
      Click Me
    </a>
  )
}

// 使用 React.forwardRef 包裝元件
const ForwardedMyButton = React.forwardRef(MyButton)

export default function Page() {
  return (
    <Link href="/about" passHref legacyBehavior>
      <ForwardedMyButton />
    </Link>
  )
}

替換 URL 而非推送

Link 元件的預設行為是將新 URL pushhistory 堆疊中。您可以使用 replace 屬性來避免添加新條目,如下例所示:

import Link from 'next/link'

export default function Page() {
  return (
    <Link href="/about" replace>
      About us
    </Link>
  )
}
import Link from 'next/link'

export default function Page() {
  return (
    <Link href="/about" replace>
      About us
    </Link>
  )
}

禁用滾動至頁面頂部

Next.js 中 <Link> 的預設滾動行為是保持滾動位置,類似於瀏覽器處理前進和後退導航的方式。當您導航至新的 Page 時,只要該頁面在視窗中可見,滾動位置將保持不變。

但是,如果頁面不在視窗的可見範圍內,Next.js 會滾動至第一個頁面元素的頂部。如果您想禁用此行為,可以將 scroll={false} 傳遞給 <Link> 元件,或將 scroll: false 傳遞給 router.push()router.replace()

import Link from 'next/link'

export default function Page() {
  return (
    <Link href="/#hashid" scroll={false}>
      禁用滾動至頂部
    </Link>
  )
}

使用 router.push()router.replace()

// useRouter
import { useRouter } from 'next/navigation'

const router = useRouter()

router.push('/dashboard', { scroll: false })

在中間層 (Middleware) 中預取連結

常見的做法是使用中間層 (Middleware)來處理驗證或其他需要將使用者重新導向到不同頁面的情況。為了讓 <Link /> 元件能正確預取透過中間層重寫的連結,你需要告訴 Next.js 要顯示的 URL 和要預取的 URL。這是為了避免不必要的向中間層發送請求來確認要預取的正確路由。

例如,如果你想提供一個具有已驗證和訪客視圖的 /dashboard 路由,可以在你的中間層中添加以下內容來將使用者重定向到正確的頁面:

import { NextResponse } from 'next/server'

export function middleware(request: Request) {
  const nextUrl = request.nextUrl
  if (nextUrl.pathname === '/dashboard') {
    if (request.cookies.authToken) {
      return NextResponse.rewrite(new URL('/auth/dashboard', request.url))
    } else {
      return NextResponse.rewrite(new URL('/public/dashboard', request.url))
    }
  }
}

在這種情況下,你需要在 <Link /> 元件中使用以下程式碼:

'use client'

import Link from 'next/link'
import useIsAuthed from './hooks/useIsAuthed' // 你的驗證鉤子

export default function Page() {
  const isAuthed = useIsAuthed()
  const path = isAuthed ? '/auth/dashboard' : '/public/dashboard'
  return (
    <Link as="/dashboard" href={path}>
      Dashboard
    </Link>
  )
}

阻止導航

你可以使用 onNavigate 屬性在某些條件滿足時阻止導航,例如當表單有未儲存的變更時。當你需要在應用程式的多個元件中阻止導航(例如在編輯表單時阻止從任何連結導航),React Context 提供了一種乾淨的方式來共享這個阻止狀態。首先,建立一個上下文來追蹤導航阻止狀態:

'use client'

import { createContext, useState, useContext } from 'react'

interface NavigationBlockerContextType {
  isBlocked: boolean
  setIsBlocked: (isBlocked: boolean) => void
}

export const NavigationBlockerContext =
  createContext<NavigationBlockerContextType>({
    isBlocked: false,
    setIsBlocked: () => {},
  })

export function NavigationBlockerProvider({
  children,
}: {
  children: React.ReactNode
}) {
  const [isBlocked, setIsBlocked] = useState(false)

  return (
    <NavigationBlockerContext.Provider value={{ isBlocked, setIsBlocked }}>
      {children}
    </NavigationBlockerContext.Provider>
  )
}

export function useNavigationBlocker() {
  return useContext(NavigationBlockerContext)
}

建立一個使用該上下文的表單元件:

'use client'

import { useNavigationBlocker } from '../contexts/navigation-blocker'

export default function Form() {
  const { setIsBlocked } = useNavigationBlocker()

  return (
    <form
      onSubmit={(e) => {
        e.preventDefault()
        setIsBlocked(false)
      }}
      onChange={() => setIsBlocked(true)}
    >
      <input type="text" name="name" />
      <button type="submit">Save</button>
    </form>
  )
}

建立一個自訂的 Link 元件來阻止導航:

'use client'

import Link from 'next/link'
import { useNavigationBlocker } from '../contexts/navigation-blocker'

interface CustomLinkProps extends React.ComponentProps<typeof Link> {
  children: React.ReactNode
}

export function CustomLink({ children, ...props }: CustomLinkProps) {
  const { isBlocked } = useNavigationBlocker()

  return (
    <Link
      onNavigate={(e) => {
        if (
          isBlocked &&
          !window.confirm('You have unsaved changes. Leave anyway?')
        ) {
          e.preventDefault()
        }
      }}
      {...props}
    >
      {children}
    </Link>
  )
}

建立一個導航元件:

'use client'

import { CustomLink as Link } from './custom-link'

export default function Nav() {
  return (
    <nav>
      <Link href="/">Home</Link>
      <Link href="/about">About</Link>
    </nav>
  )
}

最後,在根佈局中用 NavigationBlockerProvider 包裹你的應用程式,並在頁面中使用這些元件:

import { NavigationBlockerProvider } from './contexts/navigation-blocker'

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <body>
        <NavigationBlockerProvider>{children}</NavigationBlockerProvider>
      </body>
    </html>
  )
}

然後,在頁面中使用 NavForm 元件:

import Nav from './components/nav'
import Form from './components/form'

export default function Page() {
  return (
    <div>
      <Nav />
      <main>
        <h1>Welcome to the Dashboard</h1>
        <Form />
      </main>
    </div>
  )
}

當使用者嘗試在表單有未儲存的變更時使用 CustomLink 導航時,系統會提示他們確認是否離開。

版本歷史

版本變更
v15.3.0新增 onNavigate API
v13.0.0不再需要子 <a> 標籤。提供了一個程式碼修改工具 (codemod) 來自動更新你的程式碼庫。
v10.0.0指向動態路由的 href 屬性會自動解析,不再需要 as 屬性。
v8.0.0改進了預取效能。
v1.0.0引入 next/link