Next.js 13 router.events废弃后,路由跳转确认弹窗实现求助
解决方案
1. 修复useNavigationEvent钩子的pathname未定义问题
你的自定义钩子未导入usePathname,导致pathname变量未定义。修正后的完整钩子代码如下:
// components/PromptService/useNavigationEvent.tsx import { usePathname, useSearchParams } from 'next/navigation'; import { useEffect, useRef } from 'react'; export const useNavigationEvent = (onPathChange: (nextPath: string) => void) => { const pathname = usePathname(); const searchParams = useSearchParams(); const currentUrlRef = useRef(`${pathname}?${searchParams}`); useEffect(() => { const newUrl = `${pathname}?${searchParams}`; if (currentUrlRef.current !== newUrl) { onPathChange(newUrl); currentUrlRef.current = newUrl; } }, [pathname, searchParams, onPathChange]); };
2. 完整实现路由跳转拦截的PromptDialog组件
Next.js 13 App Router中,需同时处理浏览器前进后退和内部链接点击两种场景,以下是修改后的完整组件:
import { useRouter } from 'next/navigation'; import React, { useState, useEffect, useRef, useCallback } from "react"; import { Modal } from "../Modal/Modal"; import { ExclamationIcon, XIcon } from "@heroicons/react/outline"; import { Dialog } from "@headlessui/react"; import { usePathname, useSearchParams } from 'next/navigation' import { useNavigationEvent } from './useNavigationEvent'; export interface serviceProps { shouldConfirmLeave: boolean; updateParent: (...args: any[]) => any; title: string; subTitle: string; content?: string; } export const PromptDialog = ({ updateParent, shouldConfirmLeave, title, subTitle, content, }: serviceProps): React.ReactElement<serviceProps> => { const [showPromptDialog, setShowPromptDialog] = useState(false); const [nextRouterPath, setNextRouterPath] = useState<string>(); const router = useRouter(); const pathname = usePathname(); const searchParams = useSearchParams(); const isNavigating = useRef(false); // 标记导航状态,避免重复拦截 // 拦截浏览器前进/后退操作 useEffect(() => { const handleBeforePopState = () => { if (!shouldConfirmLeave || isNavigating.current) return true; updateParent("initial"); setShowPromptDialog(true); setNextRouterPath(`${pathname}?${searchParams}`); return false; // 阻止默认导航 }; router.beforePopState(handleBeforePopState); return () => router.beforePopState(() => true); // 清理钩子 }, [shouldConfirmLeave, pathname, searchParams, updateParent, router]); // 拦截内部链接点击 useEffect(() => { const handleLinkClick = (e: MouseEvent) => { if (!shouldConfirmLeave || isNavigating.current) return; const target = e.target as HTMLElement; const link = target.closest('a'); if (!link || link.target === '_blank' || link.host !== window.location.host) return; const nextPath = link.pathname + link.search; if (nextPath === `${pathname}${searchParams}`) return; e.preventDefault(); updateParent("initial"); setShowPromptDialog(true); setNextRouterPath(nextPath); }; window.addEventListener('click', handleLinkClick); return () => window.removeEventListener('click', handleLinkClick); }, [shouldConfirmLeave, pathname, searchParams, updateParent]); const cancelRouteChange = useCallback(() => { updateParent("cancel"); setNextRouterPath(undefined); setShowPromptDialog(false); isNavigating.current = false; }, [updateParent]); const confirmRouteChange = useCallback(() => { updateParent("confirm"); setShowPromptDialog(false); isNavigating.current = true; // 标记正在导航,避免重复触发拦截 if (nextRouterPath) { router.push(nextRouterPath); // 导航完成后重置标记 setTimeout(() => isNavigating.current = false, 100); } }, [updateParent, nextRouterPath, router]); return ( <Modal open={showPromptDialog} onClose={cancelRouteChange}> <div className="hidden sm:block absolute top-0 right-0 pt-4 pr-4"> <button type="button" className="bg-neutral-900 text-neutral-400 hover:text-neutral-500 focus:outline-none" onClick={cancelRouteChange} > <span className="sr-only">Close</span> <XIcon className="h-6 w-6" aria-hidden="true" /> </button> </div> <div className="sm:flex sm:items-start"> <div className="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-gradient-to-r from-orange-400 to-pink-600 sm:mx-0 sm:h-10 sm:w-10"> <ExclamationIcon className="h-6 w-6 text-white" aria-hidden="true" /> </div> <div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left"> <Dialog.Title as="h3" className="text-lg leading-6 font-medium text-white" > {title} </Dialog.Title> <div className="mt-2"> <p className="text-sm text-neutral-500">{subTitle}</p> {content && ( <p className="text-sm text-neutral-500 mt-2">{content}</p> )} </div> </div> </div> <div className="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse"> <button className="ml-4 primary-button" type="button" disabled={false} onClick={confirmRouteChange} > Continue without saving </button> <button type="button" className="rounded-md mt-3 w-full inline-flex justify-center border border-neutral-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-neutral-700 hover:text-neutral-500 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:mt-0 sm:w-auto sm:text-sm" onClick={cancelRouteChange} > Keep editing </button> </div> </Modal> ); };
3. 关键修改说明
- 移除了废弃的
router.events相关代码,替换为App Router支持的beforePopStateAPI处理浏览器导航 - 新增全局点击事件监听,拦截同域内部链接的跳转请求
- 用
isNavigating引用变量标记导航状态,避免确认跳转时重复触发拦截逻辑 - 修复了
useNavigationEvent钩子的依赖缺失问题,正确拼接完整URL进行路径变化判断
内容的提问来源于stack exchange,提问作者Joseph Braganza
相关产品推荐
相关产品推荐

