跳到内容

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 钩子,这意味着它不能与类一起使用。你可以使用 withRouter 或将你的类包装在函数组件中。

router 对象

以下是 useRouterwithRouter 返回的 router 对象的定义

  • pathname: String - /pages 之后当前路由文件的路径。因此,不包括 basePathlocale 和尾部斜杠 (trailingSlash: true)。
  • query: Object - 解析为对象的查询字符串,包括动态路由参数。如果页面不使用服务器端渲染,则在预渲染期间它将是一个空对象。默认为 {}
  • asPath: String - 浏览器中显示的路径,包括搜索参数并遵循 trailingSlash 配置。不包括 basePathlocale
  • isFallback: boolean - 当前页面是否处于回退模式
  • basePath: String - 活动的 basePath(如果已启用)。
  • locale: String - 活动的语言环境(如果已启用)。
  • locales: String[] - 所有支持的语言环境(如果已启用)。
  • defaultLocale: String - 当前默认语言环境(如果已启用)。
  • domainLocales: Array<{domain, defaultLocale, locales}> - 任何配置的域名语言环境。
  • isReady: boolean - 路由字段是否已在客户端更新并准备就绪。应仅在 useEffect 方法内部使用,而不用于在服务器上进行条件渲染。请参阅相关文档以了解与自动静态优化页面的用例
  • isPreview: boolean - 应用程序当前是否处于预览模式

如果页面使用服务器端渲染或自动静态优化进行渲染,使用 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')}>
      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: 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 }) => {
      // 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

重新加载当前 URL。相当于点击浏览器的刷新按钮。它执行 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 }) - 当哈希将更改但页面不变时触发
  • 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(
        `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 钩子相同,但可以在 apppages 目录中使用。

它与 next/router 的不同之处在于,当 pages 路由器未挂载时,它不会抛出错误,而是返回 NextRouter | null 类型。这允许开发人员在转换为 app 路由器时将组件转换为支持在 apppages 中运行。

以前看起来像这样的组件

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

当转换为 next/compat/router 时会出错,因为 null 无法解构。相反,开发人员将能够利用新的钩子

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 中使用时,你可以删除对兼容路由器的引用

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')
  // ...
}

在 Next.js 页面上下文之外使用 useRouter

另一个特殊用例是在 Next.js 应用程序上下文之外渲染组件时,例如在 pages 目录中的 getServerSideProps 内部。在这种情况下,可以使用兼容路由器来避免错误

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 一起使用,组件需要接受一个路由器属性

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)