Next.js中如何实现关闭标签页或路由跳转时的未保存更改提示?
适配Next.js的页面跳转/关闭拦截方案
针对你迁移Next.js时需要实现的两种拦截场景,以下分Pages Router和App Router两种路由系统给出对应实现方案:
1. Pages Router 方案(Next.js 12及以下,或使用Pages Router的13+项目)
基于next/router的路由事件监听,替代react-router的Prompt,同时保留浏览器关闭/刷新的拦截逻辑:
import { useEffect, useState } from 'react'; import { useRouter } from 'next/router'; const useUnsavedChangesWarning = ( message = "确定要丢弃未保存的更改吗?", // 可选:自定义路由拦截条件,比如仅拦截跳转到注册页的场景 shouldBlockRoute = () => true ) => { const [isDirty, setDirty] = useState(false); const router = useRouter(); // 拦截浏览器关闭/刷新 useEffect(() => { const handleBeforeUnload = (e) => { if (isDirty) { e.preventDefault(); e.returnValue = message; return message; } }; window.addEventListener('beforeunload', handleBeforeUnload); return () => window.removeEventListener('beforeunload', handleBeforeUnload); }, [isDirty, message]); // 拦截路由跳转 useEffect(() => { const handleRouteChangeStart = (url) => { if (isDirty && shouldBlockRoute(url)) { if (!confirm(message)) { router.events.emit('routeChangeError'); throw '路由跳转已取消'; } } }; router.events.on('routeChangeStart', handleRouteChangeStart); return () => router.events.off('routeChangeStart', handleRouteChangeStart); }, [isDirty, router, message, shouldBlockRoute]); return [() => setDirty(true), () => setDirty(false)]; }; export default useUnsavedChangesWarning;
使用示例
- 表单页面(拦截所有跳转):
import useUnsavedChangesWarning from '../hooks/useUnsavedChangesWarning'; const ApplicationForm = () => { const [setDirty, setClean] = useUnsavedChangesWarning(); // 输入变化时标记为需拦截状态 const handleInputChange = () => setDirty(); // 提交后清除拦截状态 const handleSubmit = async (e) => { e.preventDefault(); // 执行表单提交逻辑 setClean(); router.push('/success'); }; return ( <form onSubmit={handleSubmit}> <input type="text" onChange={handleInputChange} /> <button type="submit">提交</button> </form> ); };
- 仪表盘页面(仅拦截跳转到注册页):
import useUnsavedChangesWarning from '../hooks/useUnsavedChangesWarning'; import { useRouter } from 'next/router'; const Dashboard = () => { const router = useRouter(); const [setDirty] = useUnsavedChangesWarning( "你确定要返回注册页吗?确定则登出,取消则留在仪表盘", (targetUrl) => targetUrl.includes('/auth/signup') ); // 页面加载时标记为需拦截状态 useEffect(() => setDirty(), []); const handleLogout = () => { // 执行登出逻辑(清除登录状态等) router.push('/auth/signup'); }; return ( <div> <h1>仪表盘</h1> <button onClick={handleLogout}>登出</button> </div> ); };
2. App Router 方案(Next.js 13+ App Router)
App Router需标记组件为客户端组件,通过监听popstate事件处理浏览器后退,结合封装的编程式跳转方法实现拦截:
'use client'; import { useEffect, useState } from 'react'; import { useRouter, usePathname } from 'next/navigation'; const useUnsavedChangesWarning = ( message = "确定要丢弃未保存的更改吗?", shouldBlockRoute = () => true ) => { const [isDirty, setDirty] = useState(false); const router = useRouter(); const pathname = usePathname(); // 拦截浏览器关闭/刷新 useEffect(() => { const handleBeforeUnload = (e) => { if (isDirty) { e.preventDefault(); e.returnValue = message; return message; } }; window.addEventListener('beforeunload', handleBeforeUnload); return () => window.removeEventListener('beforeunload', handleBeforeUnload); }, [isDirty, message]); // 拦截浏览器后退/前进 useEffect(() => { const handlePopState = () => { if (isDirty && shouldBlockRoute()) { if (!confirm(message)) { // 回退到当前页面 router.push(pathname); } } }; window.addEventListener('popstate', handlePopState); return () => window.removeEventListener('popstate', handlePopState); }, [isDirty, router, pathname, message, shouldBlockRoute]); // 封装安全跳转方法,拦截编程式路由跳转 const safePush = (href) => { if (isDirty && shouldBlockRoute(href)) { if (confirm(message)) router.push(href); } else { router.push(href); } }; return [() => setDirty(true), () => setDirty(false), safePush]; }; export default useUnsavedChangesWarning;
使用示例(仪表盘页面):
'use client'; import useUnsavedChangesWarning from '../hooks/useUnsavedChangesWarning'; const Dashboard = () => { const [setDirty, setClean, safePush] = useUnsavedChangesWarning( "你确定要返回注册页吗?确定则登出,取消则留在仪表盘", (targetHref) => targetHref === '/auth/signup' ); useEffect(() => setDirty(), []); const handleLogout = () => { // 执行登出逻辑 safePush('/auth/signup'); }; return ( <div> <h1>仪表盘</h1> <button onClick={handleLogout}>登出</button> </div> ); };
关键注意事项
- App Router下所有使用该hook的组件必须添加
'use client'指令。 - 浏览器的
beforeunload提示文本受安全限制,部分浏览器会强制替换为默认文本,无法完全自定义。 - 仪表盘场景中,若用户确认跳转,需在
confirm的回调内先执行登出逻辑,再进行路由跳转。
内容的提问来源于stack exchange,提问作者anup
相关产品推荐
相关产品推荐

