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
对象
以下是 useRouter
和 withRouter
返回的 router
对象的定义:
pathname
:String
- 当前路由文件在/pages
之后的路径。因此,不包含basePath
、locale
和尾部斜杠 (trailingSlash: true
)。query
:Object
- 解析为对象的查询字符串,包括 动态路由 参数。如果页面不使用 服务端渲染 (SSR),则在预渲染期间为空对象。默认为{}
。asPath
:String
- 浏览器中显示的路径,包括搜索参数并遵循trailingSlash
配置。不包含basePath
和locale
。isFallback
:boolean
- 当前页面是否处于 回退模式 (fallback mode)。basePath
:String
- 当前启用的 basePath(如果启用)。locale
:String
- 当前启用的语言环境 (locale)(如果启用)。locales
:String[]
- 所有支持的语言环境 (locale)(如果启用)。defaultLocale
:String
- 当前的默认语言环境 (locale)(如果启用)。domainLocales
:Array<{domain, defaultLocale, locales}>
- 任何已配置的域名语言环境 (domain locale)。isReady
:boolean
- 路由器字段是否已在客户端更新并准备就绪。应仅在useEffect
方法中使用,不应用于在服务器上条件渲染。相关用例请参阅 自动静态优化页面 (automatically statically optimized pages) 的文档。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
- 可选修饰符,用于浏览器 URL 栏中显示的路径。在 Next.js 9.5.3 之前,此参数用于动态路由。options
- 可选对象,包含以下配置选项:scroll
- 可选布尔值,控制导航后是否滚动到页面顶部。默认为true
。shallow
: 更新当前页面的路径而不重新运行getStaticProps
、getServerSideProps
或getInitialProps
。默认为false
。locale
- 可选字符串,表示新页面的语言环境 (locale)。
对于外部 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
,适用于需要 身份验证 (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 不会卸载组件。
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 对象。适用于 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
完全相同。
参考以下示例:
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
- 允许提供与当前活动语言环境 (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
在某些情况下(例如使用 自定义服务器 (Custom Server)),你可能希望在路由器处理 popstate 之前监听并执行某些操作。
router.beforePopState(cb)
cb
- 在传入的popstate
事件上运行的函数。该函数接收事件状态作为对象,包含以下属性:url
:String
- 新状态的路由。通常是page
的名称。as
:String
- 将在浏览器中显示的 URL。options
:Object
- router.push 发送的附加选项。
如果 cb
返回 false
,Next.js 路由器将不会处理 popstate
,此时你需要自行处理。请参阅 禁用文件系统路由 (Disabling file-system routing)。
你可以使用 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
) 来订阅事件,因为它在页面跳转时不会卸载,但你可以在应用中的任何组件订阅路由器事件。
路由器事件应在组件挂载时(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
的区别在于,当页面路由未挂载时不会抛出错误,而是返回类型为 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')
// ...
}
在 pages
中的 Next.js 上下文之外使用 useRouter
另一个特定场景是在 Next.js 应用上下文之外渲染组件,例如在 pages
目录的 getServerSideProps
中。此时可以使用兼容路由来避免错误:
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.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')
// void 掉 router.push 返回的 Promise
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)