連結與導航

在 Next.js 中有四種方式可以在路由之間進行導航:

本頁將介紹如何使用這些選項,並深入探討導航的運作原理。

<Link> 是一個內建元件,它擴展了 HTML 的 <a> 標籤,提供 預取 (prefetching) 和客戶端路由導航功能。這是 Next.js 中主要且推薦的路由導航方式。

你可以從 next/link 導入它,並將 href 屬性傳遞給元件:

import Link from 'next/link'

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

你還可以傳遞其他可選屬性給 <Link>。詳見 API 參考文件

範例

連結到動態區段

當連結到 動態區段 時,你可以使用 模板字面量和插值 來生成連結列表。例如,生成部落格文章列表:

app/blog/PostList.js
import Link from 'next/link'

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

檢查當前活動連結

你可以使用 usePathname() 來判斷連結是否處於活動狀態。例如,為活動連結添加類別時,可以檢查當前的 pathname 是否與連結的 href 相符:

'use client'

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

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

  return (
    <nav>
      <ul>
        <li>
          <Link className={`link ${pathname === '/' ? 'active' : ''}`} href="/">
            首頁
          </Link>
        </li>
        <li>
          <Link
            className={`link ${pathname === '/about' ? 'active' : ''}`}
            href="/about"
          >
            關於
          </Link>
        </li>
      </ul>
    </nav>
  )
}

滾動到特定 id

Next.js App Router 的預設行為是 在導航到新路由時滾動到頁面頂部,或在向前/向後導航時保持滾動位置

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

<Link href="/dashboard#settings">設定</Link>

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

須知

  • 如果頁面在導航時不在視窗的可見範圍內,Next.js 會滾動到 頁面

禁用滾動恢復

Next.js App Router 的預設行為是 在導航到新路由時滾動到頁面頂部,或在向前/向後導航時保持滾動位置。如果你想禁用此行為,可以將 scroll={false} 傳遞給 <Link> 元件,或在 router.push()router.replace() 中設定 scroll: false

// next/link
<Link href="/dashboard" scroll={false}>
  儀表板
</Link>
// useRouter
import { useRouter } from 'next/navigation'

const router = useRouter()

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

useRouter() 鉤子

useRouter 鉤子允許你從 客戶端元件 中以程式方式變更路由。

app/page.js
'use client'

import { useRouter } from 'next/navigation'

export default function Page() {
  const router = useRouter()

  return (
    <button type="button" onClick={() => router.push('/dashboard')}>
      儀表板
    </button>
  )
}

完整的 useRouter 方法列表,請參閱 API 參考文件

建議:除非有特定需求,否則請使用 <Link> 元件在路由之間導航。

redirect 函式

對於 伺服器元件,請改用 redirect 函式。

import { redirect } from 'next/navigation'

async function fetchTeam(id: string) {
  const res = await fetch('https://...')
  if (!res.ok) return undefined
  return res.json()
}

export default async function Profile({ params }: { params: { id: string } }) {
  const team = await fetchTeam(params.id)
  if (!team) {
    redirect('/login')
  }

  // ...
}

須知

  • redirect 預設返回 307 (臨時重定向) 狀態碼。在伺服器動作中使用時,它會返回 303 (查看其他),這通常用於在 POST 請求後重定向到成功頁面。
  • redirect 內部會拋出錯誤,因此應在 try/catch 區塊外呼叫。
  • redirect 可以在渲染過程中的客戶端元件呼叫,但不能在事件處理程序中呼叫。你可以改用 useRouter 鉤子
  • redirect 也接受絕對 URL,可用於重定向到外部連結。
  • 如果你想在渲染過程之前進行重定向,請使用 next.config.js中介軟體

更多資訊請參閱 redirect API 參考文件

使用原生 History API

Next.js 允許你使用原生的 window.history.pushStatewindow.history.replaceState 方法來更新瀏覽器的歷史堆疊,而無需重新載入頁面。

pushStatereplaceState 呼叫會與 Next.js 路由器整合,讓你與 usePathnameuseSearchParams 同步。

window.history.pushState

使用它來新增一個新條目到瀏覽器的歷史堆疊中。使用者可以導航回到先前的狀態。例如,對產品列表進行排序:

'use client'

import { useSearchParams } from 'next/navigation'

export default function SortProducts() {
  const searchParams = useSearchParams()

  function updateSorting(sortOrder: string) {
    const params = new URLSearchParams(searchParams.toString())
    params.set('sort', sortOrder)
    window.history.pushState(null, '', `?${params.toString()}`)
  }

  return (
    <>
      <button onClick={() => updateSorting('asc')}>升冪排序</button>
      <button onClick={() => updateSorting('desc')}>降冪排序</button>
    </>
  )
}
'use client'

import { useSearchParams } from 'next/navigation'

export default function SortProducts() {
  const searchParams = useSearchParams()

  function updateSorting(sortOrder) {
    const params = new URLSearchParams(searchParams.toString())
    params.set('sort', sortOrder)
    window.history.pushState(null, '', `?${params.toString()}`)
  }

  return (
    <>
      <button onClick={() => updateSorting('asc')}>升冪排序</button>
      <button onClick={() => updateSorting('desc')}>降冪排序</button>
    </>
  )
}

window.history.replaceState

使用它來替換瀏覽器歷史堆疊中的當前條目。使用者無法導航回到先前的狀態。例如,切換應用程式的語言:

'use client'

import { usePathname } from 'next/navigation'

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

  function switchLocale(locale: string) {
    // 例如 '/en/about' 或 '/fr/contact'
    const newPath = `/${locale}${pathname}`
    window.history.replaceState(null, '', newPath)
  }

  return (
    <>
      <button onClick={() => switchLocale('en')}>英文</button>
      <button onClick={() => switchLocale('fr')}>法文</button>
    </>
  )
}
'use client'

import { usePathname } from 'next/navigation'

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

  function switchLocale(locale) {
    // 例如 '/en/about' 或 '/fr/contact'
    const newPath = `/${locale}${pathname}`
    window.history.replaceState(null, '', newPath)
  }

  return (
    <>
      <button onClick={() => switchLocale('en')}>英文</button>
      <button onClick={() => switchLocale('fr')}>法文</button>
    </>
  )
}

路由與導航的運作原理

App Router 使用混合方法進行路由和導航。在伺服器端,你的應用程式程式碼會根據路由區段自動進行 程式碼分割 (code splitting)。在客戶端,Next.js 會 預取 (prefetch)快取 (cache) 路由區段。這意味著,當使用者導航到新路由時,瀏覽器不會重新載入頁面,只有變更的路由區段會重新渲染 — 從而提升導航體驗和效能。

1. 程式碼分割

程式碼分割允許你將應用程式程式碼拆分為較小的套件,供瀏覽器下載和執行。這減少了每個請求傳輸的資料量和執行時間,從而提升效能。

伺服器元件 讓你的應用程式程式碼能根據路由區段自動進行程式碼分割。這意味著導航時只會載入當前路由所需的程式碼。

2. 預取

預取是一種在使用者訪問路由前,在背景預先載入路由的方式。

Next.js 中有兩種預取路由的方式:

  • <Link> 元件:當路由在使用者的視窗中可見時,會自動進行預取。預取會在頁面首次載入或通過滾動進入視窗時觸發。
  • router.prefetch():可以使用 useRouter 鉤子以程式方式預取路由。

<Link> 的預設預取行為(即未指定 prefetch 屬性或設為 null)會根據 loading.js 的使用情況而有所不同。只有共享的佈局,直到第一個 loading.js 文件的渲染「樹」中的元件會被預取並快取 30s。這減少了獲取整個動態路由的成本,意味著你可以顯示 即時載入狀態,為使用者提供更好的視覺反饋。

你可以通過將 prefetch 屬性設為 false 來禁用預取。或者,你也可以通過將 prefetch 屬性設為 true 來預取超出載入邊界的完整頁面資料。

更多資訊請參閱 <Link> API 參考文件

須知

  • 預取功能僅在生產環境中啟用,開發環境中不啟用。

3. 快取

Next.js 有一個稱為 路由器快取 (Router Cache)記憶體客戶端快取。當使用者在應用程式中導航時,預取 的路由區段和已訪問路由的 React 伺服器元件負載會被儲存在快取中。

這意味著在導航時,會盡可能重用快取,而不是向伺服器發送新請求 — 通過減少請求和傳輸的資料量來提升效能。

了解更多關於 路由器快取 的運作方式和如何配置它。

4. 部分渲染

部分渲染意味著在導航時,只有變更的路由區段會在客戶端重新渲染,任何共享的區段會被保留。

例如,在兩個兄弟路由 /dashboard/settings/dashboard/analytics 之間導航時,settingsanalytics 頁面會被渲染,而共享的 dashboard 佈局會被保留。

部分渲染的運作方式

如果沒有部分渲染,每次導航都會導致客戶端重新渲染整個頁面。僅渲染變更的區段減少了傳輸的資料量和執行時間,從而提升效能。

5. 軟導航

瀏覽器在頁面之間導航時會執行「硬導航」。Next.js App Router 實現了頁面之間的「軟導航」,確保只有變更的路由區段會重新渲染(部分渲染)。這使得客戶端的 React 狀態在導航時得以保留。

6. 向後和向前導航

預設情況下,Next.js 會在向後和向前導航時保持滾動位置,並重用 路由器快取 中的路由區段。

7. 在 pages/app/ 之間的路由

當從 pages/ 逐步遷移到 app/ 時,Next.js 路由器會自動處理兩者之間的硬導航。為了檢測從 pages/app/ 的過渡,有一個客戶端路由器過濾器,它利用對應用路由的概率檢查,偶爾會導致誤判。預設情況下,這種情況應該非常罕見,因為我們將誤判概率配置為 0.01%。這個概率可以通過 next.config.js 中的 experimental.clientRouterFilterAllowedRate 選項進行自訂。需要注意的是,降低誤判率會增加客戶端套件中生成的過濾器的大小。

另外,如果你希望完全禁用此處理並手動管理 pages/app/ 之間的路由,可以在 next.config.js 中將 experimental.clientRouterFilter 設為 false。當此功能被禁用時,任何與應用路由重疊的頁面動態路由預設將無法正確導航。