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)

如果页面使用服务端渲染或 自动静态优化 (automatic static optimization),使用 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 - 可选修饰符,用于浏览器地址栏显示的路径。在 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,适用于需要 身份验证 (authentication) 的页面:

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 是相同的。

如果不希望这种行为,你有几个选择:

  1. 使用 useEffect 手动更新每个状态。在上面的示例中,可以这样实现:

    useEffect(() => {
      setCount(0)
    }, [router.query.slug])
  2. 使用 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 会阻止在 history 堆栈中添加新的 URL 记录。

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 }) - 哈希值将变化但页面不变时触发。
  • hashChangeComplete(url, { shallow }) - 哈希值已变化但页面不变时触发。

须知:这里的 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 ? '使用' : '不使用'
        } 浅层路由`
      )
    }

    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>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 属性:

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