浏览器返回按钮触发未保存页面确认模态框无限循环问题
解决未保存更改时返回导航的模态框无限循环问题
问题现象
页面存在未保存更改时,点击浏览器返回按钮或页面「Previous」按钮,确认模态框正常弹出,但确认后模态框持续弹出陷入无限循环;通过其他链接导航则完全正常。若移除window.history.pushState代码,返回导航时模态框又完全不触发。
问题根源
- 在
popstate事件回调中调用pushState会向浏览器历史栈添加一条当前页面的记录,导致调用navigate(-1)时,实际上只是回到刚添加的这条冗余记录,再次触发popstate事件,形成循环。 - 未区分用户主动确认后的导航操作和普通的返回操作,导致确认后的导航仍被拦截逻辑触发。
解决方案
- 添加状态标记是否处于确认导航的过程中,避免此时拦截
popstate事件 - 调整
popstate事件的处理逻辑,仅在非确认状态下拦截未保存的返回操作 - 优化
handleConfirm中的返回导航逻辑,跳过之前pushState添加的冗余历史记录
修改后的代码
页面组件(EstimateRequestPage)
const EstimateRequestPage = () => { const navigate = useNavigate(); const location = useLocation(); const params = new URLSearchParams(location.search); const { isDirty, setIsDirty } = useIsDirty(); const { handleNavigate, renderModal } = usePreventLeave(isDirty); return ( <Container> <form onSubmit={handleSubmit}> {/* Form fields... */} <ButtonGroup> <PreviousButton variant="outlined" onClick={() => handleNavigate(-1)}> Previous </PreviousButton> <RequestButton variant="contained" type="submit"> Submit </RequestButton> </ButtonGroup> </form> {renderModal()} </Container> ); };
自定义Hook(usePreventLeave)
import { useState, useCallback, useEffect } from 'react'; import { useLocation, useNavigate } from 'react-router-dom'; import ConfirmationModal from '@/components/ui/modal/Modal'; export const usePreventLeave = (isDirty) => { const [isModalOpen, setIsModalOpen] = useState(false); const [pendingPath, setPendingPath] = useState(null); const [isConfirming, setIsConfirming] = useState(false); // 标记是否正在确认导航 const navigate = useNavigate(); const location = useLocation(); useEffect(() => { const handlePopState = (event) => { // 正在确认导航时不触发拦截逻辑 if (isDirty && !isConfirming) { window.history.pushState(null, '', location.pathname + location.search); setIsModalOpen(true); } }; window.history.pushState(null, '', location.pathname + location.search); window.addEventListener('popstate', handlePopState); return () => { window.removeEventListener('popstate', handlePopState); }; }, [isDirty, location.pathname, location.search, isConfirming]); const handleNavigate = useCallback( (path) => { if (isDirty) { setPendingPath(path); setIsModalOpen(true); } else { navigate(path); } }, [isDirty, navigate] ); const handleConfirm = useCallback(() => { setIsModalOpen(false); setIsConfirming(true); // 标记为确认状态,跳过后续popstate拦截 if (pendingPath === null) { // 跳过pushState添加的冗余记录,直接回到真正的上一页 navigate(-2); } else if (pendingPath) { navigate(pendingPath); } // 导航完成后重置状态 setTimeout(() => { setIsConfirming(false); setPendingPath(null); }, 100); }, [navigate, pendingPath]); const handleCancel = useCallback(() => { setIsModalOpen(false); setPendingPath(null); }, []); return { handleNavigate, renderModal: () => ( <ConfirmationModal open={isModalOpen} handleClose={handleCancel} handleConfirm={handleConfirm} messageId1="Leave page?" messageId2="Unsaved changes will be lost." confirmButtonId="confirm_button" /> ), setPendingPath, }; };
关键修改点说明
- 新增
isConfirming状态:标记用户是否正在执行确认后的导航,此时跳过popstate的拦截逻辑,避免循环触发模态框。 - 调整返回导航逻辑:当
pendingPath为null(浏览器返回按钮触发)时,调用navigate(-2)跳过之前pushState添加的冗余历史记录,直接回到真正的上一页。 - 延迟重置状态:确保导航操作完成后再恢复拦截状态,防止后续正常的返回操作被误放行。
内容的提问来源于stack exchange,提问作者27Lia
相关产品推荐
相关产品推荐

