NextJS中如何在用户点击浏览器返回键前弹出确认提示?
NextJS中拦截查询参数导航的确认提示方案
问题背景
在NextJS应用中,通过查询参数isItemDetailsOpen控制模态框的显示/隐藏:设置参数时打开模态框,移除参数时关闭。当模态框内的表单存在未保存内容时,希望用户点击浏览器返回键或尝试导航离开时弹出确认提示,但当前实现中URL会先变更,再弹出确认,即使用户取消也无法恢复原URL状态。
解决方案
要实现先确认再变更URL的效果,需要分两种场景处理:浏览器的前进/后退操作(popstate事件)和NextJS内部的路由跳转。核心思路是在导航触发前拦截,确认后再执行路由变更,取消则保持当前状态。
修改后的代码示例
import { Box, Button, Modal, ModalBody, ModalContent, ModalOverlay, ModalProps, } from '@chakra-ui/react'; import { useRouter } from 'next/router'; import { useEffect, useRef, useState } from 'react'; const ItemDetails = (props: Omit<ModalProps, 'children'>) => { return ( <Modal {...props}> <ModalOverlay /> <ModalBody> <ModalContent> <p>Hello I am an item</p> {/* 模拟表单内容 */} <input placeholder="输入内容" /> </ModalContent> </ModalBody> </Modal> ); }; const Page = () => { const router = useRouter(); const { isItemDetailsOpen } = router.query; const isOpen = isItemDetailsOpen === 'true'; // 标记是否为主动关闭模态框,避免拦截合法操作 const isClosingManually = useRef(false); // 模拟表单是否存在未保存内容 const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false); const onClose = () => { isClosingManually.current = true; const { isItemDetailsOpen, ...restOfQuery } = router.query; // 使用shallow:true避免页面重新加载 router.push({ query: restOfQuery }, undefined, { shallow: true }); // 重置标记 setTimeout(() => { isClosingManually.current = false; }, 100); }; useEffect(() => { // 拦截浏览器前进/后退的popstate事件 const handlePopState = (e: PopStateEvent) => { if (isClosingManually.current) return; if (isOpen && hasUnsavedChanges) { e.preventDefault(); const confirmLeave = window.confirm( '你正在编辑内容,确定要离开并丢失修改吗?' ); if (confirmLeave) { // 用户确认后执行路由变更 const { isItemDetailsOpen, ...restOfQuery } = router.query; router.push({ query: restOfQuery }, undefined, { shallow: true }); } else { // 用户取消,恢复当前URL状态 history.pushState(null, '', window.location.href); } } }; // 拦截NextJS内部路由跳转 const handleRouteChangeStart = (url: string) => { if (isClosingManually.current) return; if (isOpen && hasUnsavedChanges) { const confirmLeave = window.confirm( '你正在编辑内容,确定要离开并丢失修改吗?' ); if (!confirmLeave) { router.events.emit('routeChangeError'); throw 'Abort route change'; } } }; window.addEventListener('popstate', handlePopState); router.events.on('routeChangeStart', handleRouteChangeStart); return () => { window.removeEventListener('popstate', handlePopState); router.events.off('routeChangeStart', handleRouteChangeStart); }; }, [router, isOpen, hasUnsavedChanges]); return ( <Box> This is a Page <Button onClick={() => router.push( { query: { ...router.query, isItemDetailsOpen: 'true' } }, undefined, { shallow: true } ) } > Open Item Details </Button> <ItemDetails isOpen={isOpen} onClose={onClose} /> {/* 模拟表单修改状态 */} <Button onClick={() => setHasUnsavedChanges(true)}>标记为未保存</Button> </Box> ); }; export default Page;
关键实现要点
popstate事件拦截:浏览器返回/前进操作会触发该事件,这里先阻止默认行为,弹出确认框后再决定是否执行路由变更;用户取消时,用history.pushState恢复当前URL,避免历史记录异常。- 主动关闭标记:通过
useRef维护isClosingManually状态,避免拦截用户主动点击模态框关闭按钮的合法操作。 - 浅渲染优化:修改查询参数时添加
shallow: true,避免页面重新加载,提升交互体验。 - 未保存状态判断:通过
hasUnsavedChanges状态控制是否触发确认提示,仅在表单有未修改内容时拦截导航。
内容的提问来源于stack exchange,提问作者Matteo Carpi
相关产品推荐
相关产品推荐

