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 对象
以下是 useRouter 和 withRouter 返回的 router 对象的定义:
pathname:String- 当前路由文件路径,位于/pages之后。因此不包括basePath、locale和尾随斜杠(trailingSlash: true)。query:Object- 解析为对象的查询字符串,包括动态路由参数。如果页面不使用服务器端渲染,则在预渲染期间将为空对象。默认为{}asPath:String- 浏览器中显示的路径,包括搜索参数并遵循trailingSlash配置。不包括basePath和locale。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(请参阅 Node.JS URL 模块文档 了解UrlObject属性)。as:UrlObject | String- 可选的路径装饰器,将显示在浏览器 URL 栏中。在 Next.js 9.5.3 之前,这用于动态路由。options- 可选对象,具有以下配置选项:scroll- 可选布尔值,控制导航后是否滚动到页面顶部。默认为trueshallow:更新当前页面的路径,而不重新运行getStaticProps、getServerSideProps或getInitialProps。默认为falselocale- 可选字符串,指示新页面的语言环境
对于外部 URL,你不需要使用
router.push。window.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 不会卸载。
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.jsimport { useRouter } from 'next/router' export default function MyApp({ Component, pageProps }) { const router = useRouter() return <Component key={router.asPath} {...pageProps} /> }
使用 URL 对象
你可以像使用 next/link 一样使用 URL 对象。对 url 和 as 参数都有效:
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的 API 完全相同。
看看下面的示例:
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- 允许提供与活动语言环境不同的语言环境。如果为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({
/* 表单数据 */
}),
}).then((res) => {
// 对已预取的仪表板页面进行快速客户端侧转换
if (res.ok) router.push('/dashboard')
})
}, [])
useEffect(() => {
// 预取仪表板页面
router.prefetch('/dashboard')
}, [router])
return (
<form onSubmit={handleSubmit}>
{/* 表单字段 */}
<button type="submit">登录</button>
</form>
)
}router.beforePopState
在某些情况下(例如,如果使用自定义服务器),你可能希望监听 popstate 并在路由器对其执行操作之前执行某些操作。
router.beforePopState(cb)cb- 在传入的popstate事件上运行的函数。该函数接收事件状态作为一个对象,该对象具有以下属性:url:String- 新状态的路由。这通常是page的名称as:String- 将在浏览器中显示的 URLoptions: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} />
}我们在这个示例中使用自定义 App(
pages/_app.js)来订阅事件,因为它在页面导航时不会被卸载,但你可以在应用程序的任何组件上订阅路由器事件。
路由器事件应在组件挂载时注册(useEffect 或 componentDidMount / componentWillUnmount),或在事件发生时以命令式方式注册。
如果路由加载被取消(例如,通过快速连续点击两个链接),routeChangeError 将触发。传递的 err 将包含一个设置为 true 的 cancelled 属性,如下例所示:
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} />
}next/compat/router 导出
这是相同的 useRouter 钩子,但可以在 app 和 pages 目录中使用。
它与 next/router 的不同之处在于,当 pages 路由器未挂载时,它不会抛出错误,而是返回类型为 NextRouter | null。
这允许开发者在转换到 app 路由器时,使组件支持在 app 和 pages 中运行。
之前看起来像这样的组件:
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() // 可能为 null 或 NextRouter 实例
const searchParams = useSearchParams()
useEffect(() => {
if (router && !router.isReady) {
return
}
// 在 `app/` 中,searchParams 将立即准备好值,在
// `pages/` 中,它将在路由器准备就绪后可用。
const search = searchParams.get('search')
// ...
}, [router, searchParams])
// ...
}此组件现在可以在 pages 和 app 目录中工作。当组件不再在 pages 中使用时,你可以删除对兼容路由器的引用:
import { useSearchParams } from 'next/navigation'
const MyComponent = () => {
const searchParams = useSearchParams()
// 由于此组件仅在 `app/` 中使用,因此可以删除兼容路由器。
const search = searchParams.get('search')
// ...
}在 Next.js 上下文之外的 pages 中使用 useRouter
另一个特定用例是在 pages 目录的 getServerSideProps 之类的 Next.js 应用程序上下文之外渲染组件时。在这种情况下,可以使用兼容路由器以避免错误:
import { renderToString } from 'react-dom/server'
import { useRouter } from 'next/compat/router'
const MyComponent = () => {
const router = useRouter() // 可能为 null 或 NextRouter 实例
// ...
}
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.pushrouter.replacerouter.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(() => {
// 在下一行禁用 linting - 这是最干净的解决方案
// eslint-disable-next-line no-floating-promises
router.push('/login')
// void 由 router.push 返回的 Promise
if (!(user || loading)) {
void router.push('/login')
}
// 或使用异步函数,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,组件需要接受一个路由器属性:
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)