跳至內容

useRouter

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

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 物件。

  • pathnameString - 位於 /pages 之後的當前路由檔案路徑。因此,不包含 basePathlocale 和結尾斜線 (trailingSlash: true)。
  • queryObject - 解析為物件的查詢字串,包含 動態路由 參數。如果頁面未使用 伺服器端渲染,則在預渲染期間它會是一個空物件。預設為 {}
  • asPathString - 瀏覽器中顯示的路徑,包含搜尋參數並遵循 trailingSlash 設定。不包含 basePathlocale
  • isFallbackboolean - 當前頁面是否處於 後備模式 (fallback mode)
  • basePathString - 啟用時的 basePath
  • localeString - 啟用時的目前語系。
  • localesString[] - 所有支援的語系(如果已啟用)。
  • defaultLocaleString - 目前的預設語系(如果已啟用)。
  • domainLocalesArray<{domain, defaultLocale, locales}> - 任何已設定的網域語系。
  • isReadyboolean - 路由欄位是否已在客戶端更新且可供使用。應僅在 useEffect 方法內使用,而不是用於在伺服器上進行條件渲染。有關使用案例,請參閱 自動靜態優化頁面 的相關文件。
  • isPreviewboolean - 應用程式目前是否處於 預覽模式

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

router 物件包含以下方法:

router.push next/link 不夠用的情況。

router.push(url, as, options)
  • urlUrlObject | String - 要導覽到的 URL(關於 UrlObject 的屬性,請參閱 Node.JS URL 模組文件)。
  • asUrlObject | String - 將顯示在瀏覽器網址列中的路徑的選用裝飾器。在 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')}>
      Click me
    </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')}>
      Click me
    </button>
  )
}

將使用者重新導向到 pages/login.js,適用於驗證後的頁面

import { useEffect } from 'react'
import { useRouter } from 'next/router'
 
// Here you would fetch and return the user
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>Redirecting...</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>Page: {router.query.slug}</h1>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increase count</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 },
        })
      }}
    >
      Click here to read more
    </button>
  )
}

router.replace

next/link 中的 replace 屬性類似,router.replace 會阻止在 history 堆疊中新增新的 URL 項目。

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

參考以下範例:

import { useRouter } from 'next/router'
 
export default function Page() {
  const router = useRouter()
 
  return (
    <button type="button" onClick={() => router.replace('/home')}>
      Click me
    </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 - 允許提供與目前使用的語系不同的語系。如果為 false,則 url 必須包含語系,因為不會使用目前使用的語系。

假設您有一個登入頁面,在登入後,您會將使用者重新導向至儀表板。在這種情況下,我們可以預先擷取儀表板以加快轉換速度,如下例所示:

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({
        /* Form data */
      }),
    }).then((res) => {
      // Do a fast client-side transition to the already prefetched dashboard page
      if (res.ok) router.push('/dashboard')
    })
  }, [])
 
  useEffect(() => {
    // Prefetch the dashboard page
    router.prefetch('/dashboard')
  }, [router])
 
  return (
    <form onSubmit={handleSubmit}>
      {/* Form fields */}
      <button type="submit">Login</button>
    </form>
  )
}

router.beforePopState

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

router.beforePopState(cb)
  • cb - 在傳入 popstate 事件時執行的函式。該函式會接收事件的狀態作為具有以下屬性的物件:
    • url字串 - 新狀態的路由。這通常是 page 的名稱。
    • as字串 - 將在瀏覽器中顯示的網址。
    • options物件 - 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 }) => {
      // I only want to allow these two routes!
      if (as !== '/' && as !== '/other') {
        // Have SSR render bad routes as a 404.
        window.location.href = as
        return false
      }
 
      return true
    })
  }, [router])
 
  return <p>Welcome to the page</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()}>
      Click here to go back
    </button>
  )
}

router.reload

重新載入目前的網址。相當於點擊瀏覽器的重新整理按鈕。它會執行 window.location.reload()

import { useRouter } from 'next/router'
 
export default function Page() {
  const router = useRouter()
 
  return (
    <button type="button" onClick={() => router.reload()}>
      Click here to 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 是瀏覽器中顯示的網址,包含 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(
        `App is changing to ${url} ${
          shallow ? 'with' : 'without'
        } shallow routing`
      )
    }
 
    router.events.on('routeChangeStart', handleRouteChange)
 
    // If the component is unmounted, unsubscribe
    // from the event with the `off` method:
    return () => {
      router.events.off('routeChangeStart', handleRouteChange)
    }
  }, [router])
 
  return <Component {...pageProps} />
}

在此範例中,我們使用自訂應用程式 (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(`Route to ${url} was cancelled!`)
      }
    }
 
    router.events.on('routeChangeError', handleRouteChangeError)
 
    // If the component is unmounted, unsubscribe
    // from the event with the `off` method:
    return () => {
      router.events.off('routeChangeError', handleRouteChangeError)
    }
  }, [router])
 
  return <Component {...pageProps} />
}

next/compat/router 匯出

這是相同的 useRouter hook,但可以在 apppages 目錄中使用。

它與 next/router 的不同之處在於,當 pages 路由器未掛載時,它不會拋出錯誤,而是返回 NextRouter | null 類型。這允許開發人員在轉換到 app 路由器時,將組件轉換為支援在 apppages 中運行。

先前看起來像這樣的組件

import { useRouter } from 'next/router'
const MyComponent = () => {
  const { isReady, query } = useRouter()
  // ...
}

轉換到 next/compat/router 時會出錯,因為 null 無法被解構。開發人員將能夠利用新的 hooks

import { useEffect } from 'react'
import { useRouter } from 'next/compat/router'
import { useSearchParams } from 'next/navigation'
const MyComponent = () => {
  const router = useRouter() // may be null or a NextRouter instance
  const searchParams = useSearchParams()
  useEffect(() => {
    if (router && !router.isReady) {
      return
    }
    // In `app/`, searchParams will be ready immediately with the values, in
    // `pages/` it will be available after the router is ready.
    const search = searchParams.get('search')
    // ...
  }, [router, searchParams])
  // ...
}

此組件現在可以在 pagesapp 目錄中運作。當組件不再在 pages 中使用時,您可以移除對 compat 路由器的引用

import { useSearchParams } from 'next/navigation'
const MyComponent = () => {
  const searchParams = useSearchParams()
  // As this component is only used in `app/`, the compat router can be removed.
  const search = searchParams.get('search')
  // ...
}

在 pages 中的 Next.js 上下文之外使用 useRouter

另一個特定的用例是在 Next.js 應用程式上下文之外渲染組件時,例如在 pages 目錄的 getServerSideProps 內。在這種情況下,可以使用 compat 路由器來避免錯誤

import { renderToString } from 'react-dom/server'
import { useRouter } from 'next/compat/router'
const MyComponent = () => {
  const router = useRouter() // may be null or a NextRouter instance
  // ...
}
export async function getServerSideProps() {
  const renderedComponent = renderToString(<MyComponent />)
  return {
    props: {
      renderedComponent,
    },
  }
}

潛在的 ESLint 錯誤

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

如果您的應用程式需要此規則,您應該使用 void 處理 Promise,或者使用 async 函式,以 await 等待 Promise,然後使用 void 處理函式呼叫。當從 onClick 事件處理程式內部呼叫方法時,這不適用。

受影響的方法如下:

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

可能的解決方案

import { useEffect } from 'react'
import { useRouter } from 'next/router'
 
// Here you would fetch and return the user
const useUser = () => ({ user: null, loading: false })
 
export default function Page() {
  const { user, loading } = useUser()
  const router = useRouter()
 
  useEffect(() => {
    // disable the linting on the next line - This is the cleanest solution
    // eslint-disable-next-line no-floating-promises
    router.push('/login')
 
    // void the Promise returned by router.push
    if (!(user || loading)) {
      void router.push('/login')
    }
    // or use an async function, await the Promise, then void the function call
    async function handleRouteChange() {
      if (!(user || loading)) {
        await router.push('/login')
      }
    }
    void handleRouteChange()
  }, [user, loading])
 
  return <p>Redirecting...</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)