useRouter

如果您想在應用程式的任何函式元件中存取 router 物件,可以使用 useRouter 鉤子,請參考以下範例:

import { useRouter } from 'next/router'

function ActiveLink({ children, href }) {
  const router = useRouter()
  const style = {
    marginRight: 10,
    color: router.asPath === href ? 'red' : 'black',
  }

  const handleClick = (e) => {
    e.preventDefault()
    router.push(href)
  }

  return (
    <a href={href} onClick={handleClick} style={style}>
      {children}
    </a>
  )
}

export default ActiveLink

useRouter 是一個 React Hook,意味著它不能與類別一起使用。您可以使用 withRouter 或將您的類別包裝在函式元件中。

router 物件

以下是 useRouterwithRouter 返回的 router 物件的定義:

  • pathname: String - 當前路由檔案的路徑,位於 /pages 之後。因此不包含 basePathlocale 和結尾斜線 (trailingSlash: true)。
  • query: Object - 解析為物件的查詢字串,包含動態路由參數。如果頁面未使用伺服器端渲染 (SSR),預渲染期間將為空物件。預設為 {}
  • asPath: String - 瀏覽器中顯示的路徑,包含搜尋參數並遵循 trailingSlash 配置。不包含 basePathlocale
  • isFallback: boolean - 當前頁面是否處於後備模式 (fallback mode)
  • basePath: String - 當前啟用的 basePath(如果已啟用)。
  • locale: String - 當前啟用的語言環境(如果已啟用)。
  • locales: String[] - 所有支援的語言環境(如果已啟用)。
  • defaultLocale: String - 當前預設語言環境(如果已啟用)。
  • domainLocales: Array<{domain, defaultLocale, locales}> - 任何已配置的網域語言環境。
  • isReady: boolean - 路由器欄位是否已在客戶端更新並可供使用。應僅在 useEffect 方法中使用,不用於伺服器端的條件渲染。相關使用案例請參閱自動靜態優化頁面的文件。
  • isPreview: boolean - 應用程式當前是否處於預覽模式 (preview mode)

如果頁面使用伺服器端渲染或自動靜態優化,使用 asPath 欄位可能導致客戶端與伺服器不一致。在 isReady 欄位為 true 之前,避免使用 asPath

router 中包含以下方法:

router.push

處理客戶端轉換,此方法適用於 next/link 不足的情況。

router.push(url, as, options)
  • url: UrlObject | String - 要導航到的 URL(UrlObject 屬性請參閱 Node.JS URL 模組文件)。
  • as: UrlObject | String - 瀏覽器 URL 欄中顯示的路徑的可選修飾符。在 Next.js 9.5.3 之前,這用於動態路由。
  • options - 可選物件,包含以下配置選項:
    • scroll - 可選布林值,控制導航後是否滾動到頁面頂部。預設為 true
    • shallow: 更新當前頁面的路徑而不重新執行 getStaticPropsgetServerSidePropsgetInitialProps。預設為 false
    • locale - 可選字串,表示新頁面的語言環境

對於外部 URL,您不需要使用 router.pushwindow.location 更適合這些情況。

導航到預定義路由 pages/about.js

import { useRouter } from 'next/router'

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

  return (
    <button type="button" onClick={() => router.push('/about')}>
      點擊我
    </button>
  )
}

導航到動態路由 pages/post/[pid].js

import { useRouter } from 'next/router'

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

  return (
    <button type="button" onClick={() => router.push('/post/abc')}>
      點擊我
    </button>
  )
}

將用戶重定向到 pages/login.js,適用於身份驗證後的頁面:

import { useEffect } from 'react'
import { useRouter } from 'next/router'

// 這裡您會獲取並返回用戶
const useUser = () => ({ user: null, loading: false })

export default function Page() {
  const { user, loading } = useUser()
  const router = useRouter()

  useEffect(() => {
    if (!(user || loading)) {
      router.push('/login')
    }
  }, [user, loading])

  return <p>正在重新導向...</p>
}

導航後重置狀態

在 Next.js 中導航到同一頁面時,頁面狀態不會預設重置,因為除非父元件發生變化,否則 React 不會卸載元件。

pages/[slug].js
import Link from 'next/link'
import { useState } from 'react'
import { useRouter } from 'next/router'

export default function Page(props) {
  const router = useRouter()
  const [count, setCount] = useState(0)
  return (
    <div>
      <h1>頁面: {router.query.slug}</h1>
      <p>計數: {count}</p>
      <button onClick={() => setCount(count + 1)}>增加計數</button>
      <Link href="/one">one</Link> <Link href="/two">two</Link>
    </div>
  )
}

在上面的範例中,在 /one/two 之間導航不會重置計數。useState 在渲染之間保持不變,因為頂層 React 元件 Page 是相同的。

如果您不希望這種行為,您有幾個選項:

  • 使用 useEffect 手動確保每個狀態更新。在上面的範例中,可以這樣做:

    useEffect(() => {
      setCount(0)
    }, [router.query.slug])
  • 使用 React key告訴 React 重新掛載元件。要對所有頁面執行此操作,可以使用自訂應用程式:

    pages/_app.js
    import { useRouter } from 'next/router'
    
    export default function MyApp({ Component, pageProps }) {
      const router = useRouter()
      return <Component key={router.asPath} {...pageProps} />
    }

使用 URL 物件

您可以像在 next/link 中一樣使用 URL 物件。適用於 urlas 參數:

import { useRouter } from 'next/router'

export default function ReadMore({ post }) {
  const router = useRouter()

  return (
    <button
      type="button"
      onClick={() => {
        router.push({
          pathname: '/post/[pid]',
          query: { pid: post.id },
        })
      }}
    >
      點擊這裡閱讀更多
    </button>
  )
}

router.replace

類似於 next/link 中的 replace 屬性,router.replace 會阻止將新的 URL 條目添加到 history 堆疊中。

router.replace(url, as, options)
  • router.replace 的 API 與 router.push 完全相同。

請看以下範例:

import { useRouter } from 'next/router'

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

  return (
    <button type="button" onClick={() => router.replace('/home')}>
      點擊我
    </button>
  )
}

router.prefetch

預取頁面以實現更快的客戶端轉換。此方法僅適用於沒有 next/link 的導航,因為 next/link 會自動處理頁面預取。

這是一個僅在生產環境中有效的功能。Next.js 在開發環境中不會預取頁面。

router.prefetch(url, as, options)
  • url - 要預取的 URL,包括明確路由(例如 /dashboard)和動態路由(例如 /product/[id]
  • as - url 的可選修飾符。在 Next.js 9.5.3 之前,這用於預取動態路由。
  • options - 可選物件,包含以下允許的欄位:
    • locale - 允許提供與當前語言環境不同的語言環境。如果為 falseurl 必須包含語言環境,因為不會使用當前語言環境。

假設您有一個登入頁面,登入後將用戶重定向到儀表板。對於這種情況,我們可以預取儀表板以實現更快的轉換,如下例所示:

import { useCallback, useEffect } from 'react'
import { useRouter } from 'next/router'

export default function Login() {
  const router = useRouter()
  const handleSubmit = useCallback((e) => {
    e.preventDefault()

    fetch('/api/login', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        /* 表單數據 */
      }),
    }).then((res) => {
      // 快速客戶端轉換到已預取的儀表板頁面
      if (res.ok) router.push('/dashboard')
    })
  }, [])

  useEffect(() => {
    // 預取儀表板頁面
    router.prefetch('/dashboard')
  }, [router])

  return (
    <form onSubmit={handleSubmit}>
      {/* 表單欄位 */}
      <button type="submit">登入</button>
    </form>
  )
}

router.beforePopState

在某些情況下(例如使用自訂伺服器 (Custom Server)),您可能希望監聽 popstate 並在路由器處理之前執行某些操作。

router.beforePopState(cb)
  • cb - 在傳入 popstate 事件時運行的函式。該函式接收事件的狀態作為物件,包含以下屬性:
    • url: String - 新狀態的路由。通常是 page 的名稱
    • as: String - 將在瀏覽器中顯示的 URL
    • options: Object - router.push 發送的附加選項

如果 cb 返回 false,Next.js 路由器將不會處理 popstate,您將負責在這種情況下處理它。請參閱禁用文件系統路由

您可以使用 beforePopState 來操作請求或強制 SSR 刷新,如下例所示:

import { useEffect } from 'react'
import { useRouter } from 'next/router'

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

  useEffect(() => {
    router.beforePopState(({ url, as, options }) => {
      // 我只想允許這兩個路由!
      if (as !== '/' && as !== '/other') {
        // 讓 SSR 將錯誤路由渲染為 404。
        window.location.href = as
        return false
      }

      return true
    })
  }, [router])

  return <p>歡迎來到頁面</p>
}

router.back

在歷史記錄中返回。等同於點擊瀏覽器的返回按鈕。它執行 window.history.back()

import { useRouter } from 'next/router'

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

  return (
    <button type="button" onClick={() => router.back()}>
      點擊這裡返回
    </button>
  )
}

router.reload

重新載入當前 URL。等同於點擊瀏覽器的刷新按鈕。它執行 window.location.reload()

import { useRouter } from 'next/router'

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

  return (
    <button type="button" onClick={() => router.reload()}>
      點擊這裡重新載入
    </button>
  )
}

router.events

您可以監聽 Next.js 路由器內發生的不同事件。以下是支援的事件列表:

  • routeChangeStart(url, { shallow }) - 當路由開始改變時觸發
  • routeChangeComplete(url, { shallow }) - 當路由完全改變時觸發
  • routeChangeError(err, url, { shallow }) - 當改變路由時發生錯誤或路由載入被取消時觸發
    • err.cancelled - 指示導航是否被取消
  • beforeHistoryChange(url, { shallow }) - 在改變瀏覽器歷史記錄之前觸發
  • hashChangeStart(url, { shallow }) - 當 hash 將改變但頁面不變時觸發
  • hashChangeComplete(url, { shallow }) - 當 hash 已改變但頁面不變時觸發

重要提示:這裡的 url 是瀏覽器中顯示的 URL,包含 basePath

例如,要監聽路由器事件 routeChangeStart,打開或創建 pages/_app.js 並訂閱該事件,如下所示:

import { useEffect } from 'react'
import { useRouter } from 'next/router'

export default function MyApp({ Component, pageProps }) {
  const router = useRouter()

  useEffect(() => {
    const handleRouteChange = (url, { shallow }) => {
      console.log(
        `應用程式正在切換到 ${url} ${
          shallow ? '使用' : '未使用'
        } shallow routing`
      )
    }

    router.events.on('routeChangeStart', handleRouteChange)

    // 如果元件卸載,使用 `off` 方法取消訂閱事件:
    return () => {
      router.events.off('routeChangeStart', handleRouteChange)
    }
  }, [router])

  return <Component {...pageProps} />
}

我們在此範例中使用自訂應用程式 (Custom App) (pages/_app.js) 來訂閱事件,因為它在頁面導航時不會卸載,但您可以在應用程式的任何元件中訂閱路由器事件。

路由器事件應在元件掛載時註冊(useEffectcomponentDidMount / componentWillUnmount)或在事件發生時強制註冊。

如果路由載入被取消(例如,快速連續點擊兩個連結),routeChangeError 將觸發。傳遞的 err 將包含一個設為 truecancelled 屬性,如下例所示:

import { useEffect } from 'react'
import { useRouter } from 'next/router'

export default function MyApp({ Component, pageProps }) {
  const router = useRouter()

  useEffect(() => {
    const handleRouteChangeError = (err, url) => {
      if (err.cancelled) {
        console.log(`前往 ${url} 的路由被取消!`)
      }
    }

    router.events.on('routeChangeError', handleRouteChangeError)

    // 如果元件卸載,使用 `off` 方法取消訂閱事件:
    return () => {
      router.events.off('routeChangeError', handleRouteChangeError)
    }
  }, [router])

  return <Component {...pageProps} />
}

潛在的 ESLint 錯誤

router 物件上某些可存取的方法會回傳 Promise。如果您啟用了 ESLint 規則 no-floating-promises,可以考慮全域停用此規則,或僅在受影響的行中停用。

如果您的應用程式需要此規則,您應該對 Promise 使用 void 操作符 — 或者使用 async 函式、await 該 Promise,然後對函式呼叫使用 void。當方法是在 onClick 處理函式內部呼叫時,此方法不適用

受影響的方法包括:

  • router.push
  • router.replace
  • router.prefetch

潛在解決方案

import { useEffect } from 'react'
import { useRouter } from 'next/router'

// 這裡您會取得並回傳使用者
const useUser = () => ({ user: null, loading: false })

export default function Page() {
  const { user, loading } = useUser()
  const router = useRouter()

  useEffect(() => {
    // 在下一行停用 lint 檢查 - 這是最簡潔的解決方案
    // eslint-disable-next-line no-floating-promises
    router.push('/login')

    // 對 router.push 回傳的 Promise 使用 void
    if (!(user || loading)) {
      void router.push('/login')
    }
    // 或使用 async 函式,await Promise,然後對函式呼叫使用 void
    async function handleRouteChange() {
      if (!(user || loading)) {
        await router.push('/login')
      }
    }
    void handleRouteChange()
  }, [user, loading])

  return <p>重新導向中...</p>
}

withRouter

如果 useRouter 不符合您的需求,withRouter 也可以將相同的 router 物件 新增至任何元件。

使用方式

import { withRouter } from 'next/router'

function Page({ router }) {
  return <p>{router.pathname}</p>
}

export default withRouter(Page)

TypeScript

要搭配 withRouter 使用類別元件,該元件需要接受 router prop:

import React from 'react'
import { withRouter, NextRouter } from 'next/router'

interface WithRouterProps {
  router: NextRouter
}

interface MyComponentProps extends WithRouterProps {}

class MyComponent extends React.Component<MyComponentProps> {
  render() {
    return <p>{this.props.router.pathname}</p>
  }
}

export default withRouter(MyComponent)

On this page