You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.08 08:07:58