0

0

Next.js与Chakra UI:实现页面跳转前未保存更改确认对话框

霞舞

霞舞

发布时间:2025-11-18 19:19:13

|

802人浏览过

|

来源于php中文网

原创

next.js与chakra ui:实现页面跳转前未保存更改确认对话框

本文详细介绍了在Next.js应用中,如何利用自定义React Hook和Chakra UI的AlertDialog组件,实现用户在离开带有未保存更改的页面时,弹出确认对话框的功能。通过巧妙地拦截Next.js路由事件并管理页面状态,确保用户在数据丢失前得到提示,并可选择取消跳转或继续导航至目标路由。

在现代Web应用中,提供良好的用户体验至关重要。其中一个常见场景是,当用户在表单中输入了数据但尚未保存时,意外点击其他链接或尝试返回上一页,导致数据丢失。为了避免这种情况,我们通常需要一个机制来拦截页面跳转,并弹出一个确认对话框,询问用户是否要放弃未保存的更改。本教程将深入探讨如何在Next.js应用中结合Chakra UI实现这一功能。

核心挑战:Next.js路由拦截

在React应用中,传统的浏览器API如window.onbeforeunload可以用于在用户关闭页面或刷新时触发确认。然而,对于单页应用(SPA)如Next.js,内部路由跳转并不会触发onbeforeunload。Next.js提供了router.events来监听路由生命周期事件,其中routeChangeStart是拦截路由跳转的关键。

直接在routeChangeStart事件处理器中调用event.preventDefault()并不能完全阻止Next.js的路由跳转。Next.js的内部路由机制更为复杂,它会继续尝试完成导航。因此,我们需要一个更巧妙的方法来“欺骗”Next.js,使其认为路由跳转失败,从而停止导航过程。

解决方案:自定义导航观察器Hook (useNavigationObserver)

为了优雅地解决Next.js的路由拦截问题,我们设计一个名为useNavigationObserver的自定义Hook。这个Hook将负责监听路由变化、阻止导航、显示确认对话框,并在用户确认后恢复导航。

1. useNavigationObserver Hook 的设计理念

该Hook的核心思想是:

  • 在routeChangeStart事件发生时,如果存在未保存的更改,则阻止路由继续。
  • 通过抛出一个“假”错误并捕获它,来阻止Next.js的内部导航。
  • 将目标路由保存起来,以便用户确认后可以恢复导航。
  • 提供一个回调函数,用于触发外部的确认对话框。
// hooks/useNavigationObserver.ts
import { useRouter } from "next/router";
import { useCallback, useEffect, useRef } from "react";

// 定义一个特殊的错误消息,用于在Promise拒绝时识别并阻止默认行为
const errorMessage = "Please ignore this error.";

// 抛出一个假错误来中断Next.js的路由流程
const throwFakeErrorToFoolNextRouter = () => {
  // eslint-disable-next-line no-throw-literal
  throw errorMessage;
};

// 监听未处理的Promise拒绝事件,如果原因是我们的假错误,则阻止默认行为
const rejectionHandler = (event: PromiseRejectionEvent) => {
  if (event.reason === errorMessage) {
    event.preventDefault();
  }
};

interface Props {
  shouldStopNavigation: boolean; // 是否应该阻止导航的布尔值
  onNavigate: () => void;       // 当导航被阻止时调用的回调函数(用于打开对话框)
}

const useNavigationObserver = ({ shouldStopNavigation, onNavigate }: Props) => {
  const router = useRouter();
  const currentPath = router.asPath; // 当前页面的路径
  const nextPath = useRef("");       // 存储即将跳转的目标路径
  const navigationConfirmed = useRef(false); // 标记用户是否已确认导航

  // 阻止路由事件并抛出假错误
  const killRouterEvent = useCallback(() => {
    // 触发一个路由错误事件,告知Next.js路由更改失败
    router.events.emit("routeChangeError", "", "", { shallow: false });
    throwFakeErrorToFoolNextRouter(); // 抛出假错误,中断Promise链
  }, [router]);

  useEffect(() => {
    navigationConfirmed.current = false; // 每次组件挂载或依赖更新时重置确认状态

    const onRouteChange = (url: string) => {
      // 如果当前路径与目标路径不同,且未确认导航,则进行拦截
      if (
        shouldStopNavigation &&
        url !== currentPath &&
        !navigationConfirmed.current
      ) {
        // 将浏览器的历史状态推回当前页面,以防止URL在地址栏中改变
        // 这一步很重要,因为Next.js在routeChangeStart时可能会更新URL
        window.history.pushState(null, "", router.basePath + currentPath);

        nextPath.current = url.replace(router.basePath, ""); // 存储目标路径
        onNavigate(); // 调用外部回调,通常用于打开确认对话框
        killRouterEvent(); // 阻止路由跳转
      }
    };

    // 监听路由开始变化事件
    router.events.on("routeChangeStart", onRouteChange);
    // 监听未处理的Promise拒绝事件,以捕获并处理我们的假错误
    window.addEventListener("unhandledrejection", rejectionHandler);

    return () => {
      // 组件卸载时移除事件监听器
      router.events.off("routeChangeStart", onRouteChange);
      window.removeEventListener("unhandledrejection", rejectionHandler);
    };
  }, [
    currentPath,
    killRouterEvent,
    onNavigate,
    router.basePath,
    router.events,
    shouldStopNavigation,
  ]);

  // 用户确认离开后,调用此函数继续导航
  const confirmNavigation = () => {
    navigationConfirmed.current = true; // 标记已确认
    router.push(nextPath.current);     // 跳转到之前存储的目标路径
  };

  return confirmNavigation; // 返回确认导航的函数
};

export { useNavigationObserver };

关键点解析:

Vondy
Vondy

下一代AI应用平台,汇集了一流的工具/应用程序

下载
  • throwFakeErrorToFoolNextRouter 和 rejectionHandler: 这是阻止Next.js路由的关键技巧。当routeChangeStart被触发且我们决定阻止导航时,killRouterEvent会抛出一个特殊的错误。由于Next.js的路由逻辑是基于Promise的,这个错误会导致Promise被拒绝。window.addEventListener("unhandledrejection", rejectionHandler)会捕获这个未处理的Promise拒绝,并通过event.preventDefault()阻止浏览器报告这个错误,从而避免应用崩溃。
  • window.history.pushState: 在拦截路由后,Next.js可能已经更新了浏览器的URL。为了保持URL与当前页面一致,我们需要手动将历史状态推回当前路径。
  • navigationConfirmed: 这是一个useRef变量,用于标记用户是否已经通过对话框确认了导航。如果已确认,下次routeChangeStart事件发生时就不会再次拦截。
  • confirmNavigation: 这个函数由Hook返回,当用户在确认对话框中选择“是”时调用,它会使用router.push将用户导航到之前保存的目标路径。

在组件中集成Hook和Chakra UI对话框

现在,我们将useNavigationObserver Hook集成到我们的Next.js页面组件中,并结合Chakra UI的AlertDialog来显示确认对话框。

// components/RecordEditing.tsx (示例组件)
import { useState, useEffect } from "react";
import {
  Box,
  Grid,
  GridItem,
  Input,
  Flex,
  Button,
  useColorModeValue,
  useDisclosure,
} from "@chakra-ui/react";
// 假设 AlertModal 是一个封装了 Chakra UI AlertDialog 的组件
import AlertModal from "./AlertModal"; // 自定义的对话框组件
import { useRouter } from "next/router";
import isDeepEqual from "deep-equal"; // 用于深度比较对象是否相等
import { useNavigationObserver } from "@/hooks/useNavigationObserver"; // 引入自定义Hook

// 假设的类型定义
interface IRecordEditData {
  title: string;
  url: string;
  username: string;
  password?: string;
}
interface IRecordEditingProps {
  type: "new" | "edit";
  record: IRecordEditData;
  user: { id: string };
}

const RecordEditing: React.FC = ({
  type,
  record,
  user,
}) => {
  const [recordObj, setRecordObj] = useState(record);
  const [password, setPassword] = useState(record.password || "");
  const [isDirty, setIsDirty] = useState(false); // 标记是否有未保存的更改

  // 结合初始记录和密码,作为比较的基础
  const defaultRecord = { ...record, password: record.password || "" };
  const title = type === "new" ? "New Record" : "Edit Record";
  const router = useRouter();
  const { isOpen, onOpen, onClose } = useDisclosure(); // Chakra UI对话框控制

  // 使用自定义Hook
  const navigate = useNavigationObserver({
    shouldStopNavigation: isDirty, // 只有当isDirty为true时才阻止导航
    onNavigate: () => onOpen(),     // 当导航被阻止时,打开Chakra UI对话框
  });

  // 检查当前表单数据与初始数据是否一致,更新isDirty状态
  const setDirtyInputs = () => {
    if (!isDeepEqual(defaultRecord, { ...recordObj, password })) {
      setIsDirty(true);
    } else {
      setIsDirty(false);
    }
  };

  // 处理输入框变化
  const handleInputChange = (e: React.ChangeEvent) => {
    setRecordObj((prevState) => ({
      ...prevState,
      [e.target.id]: e.target.value,
    }));
    setDirtyInputs(); // 每次输入变化后检查脏状态
  };

  // 处理表单提交
  const handleSubmit = () => {
    setIsDirty(false); // 保存后清除脏状态
    // ... (保存逻辑,例如调用API)
    // 假设保存成功后跳转到首页
    router.push("/");
  };

  // 在组件渲染时,如果 AlertModal 是一个独立的组件,需要确保它能接收到正确的props
  // 这里假设 AlertModal 内部处理了 AlertDialog 的渲染逻辑
  // 并且通过 callBackAction 属性接收 navigate 函数
  return (
    
      {/* ... 页面内容,例如输入框和保存按钮 */}
      
        
          
            
              Title
            
          
          
            
          
          {/* 其他输入字段 */}
        
      
      {/* ... PasswordEditor 组件 */}
      
        
      

      {/* 确认离开对话框 */}
      
    
  );
};

export default RecordEditing;

AlertModal 组件示例 (假设)

为了保持主组件的简洁性,我们可以将Chakra UI的AlertDialog封装到一个独立的AlertModal组件中。

// components/AlertModal.tsx
import {
  AlertDialog,
  AlertDialogBody,
  AlertDialogFooter,
  AlertDialogHeader,
  AlertDialogContent,
  AlertDialogOverlay,
  Button,
} from "@chakra-ui/react";
import React, { useRef } from "react";

interface AlertModalProps {
  isOpen: boolean;
  onClose: () => void;
  type: "leave" | "delete"; // 可以根据需要扩展类型
  callBackAction?: () => void; // 用户确认后执行的回调
}

const AlertModal: React.FC = ({
  isOpen,
  onClose,
  type,
  callBackAction,
}) => {
  const cancelRef = useRef(null);

  const handleConfirm = () => {
    if (callBackAction) {
      callBackAction(); // 执行确认操作,例如继续导航
    }
    onClose(); // 关闭对话框
  };

  const getHeader = () => {
    if (type === "leave") {
      return "确认离开此页面?";
    }
    // ... 其他类型
    return "确认操作?";
  };

  const getBody = () => {
    if (type === "leave") {
      return "您有未保存的更改。确定要离开此页面吗?未保存的更改将会丢失!";
    }
    // ... 其他类型
    return "您确定要执行此操作吗?";
  };

  return (
    
      
      
        {getHeader()}
        {getBody()}
        
          
          
        
      
    
  );
};

export default AlertModal;

集成关键点:

  • isDirty 状态管理: RecordEditing 组件需要一个状态变量isDirty来跟踪表单数据是否与初始数据不同。setDirtyInputs函数负责在每次输入变化后更新此状态。
  • useDisclosure: Chakra UI的useDisclosure Hook用于方便地管理对话框的isOpen状态以及onOpen和onClose函数。
  • onNavigate 回调: useNavigationObserver Hook的onNavigate属性被设置为() => onOpen(),这意味着当Hook决定阻止导航时,它会调用onOpen()来显示确认对话框。
  • callBackAction 属性: AlertModal组件通过callBackAction属性接收useNavigationObserver返回的navigate函数。当用户在对话框中点击“是”时,handleConfirm会调用navigate(),从而恢复被阻止的路由跳转。

注意事项与最佳实践

  1. deep-equal 库: 在实际应用中,比较复杂对象(如表单数据)是否相等,通常不能简单地使用===。deep-equal或Lodash的isEqual等库可以进行深度比较,确保isDirty状态的准确性。
  2. 错误处理: throwFakeErrorToFoolNextRouter方法虽然有效,但本质上是利用了Next.js内部机制的一个“漏洞”。未来Next.js版本更新可能会改变其内部路由行为,导致此方法失效。在使用时需注意其潜在的维护风险。
  3. 用户体验: 确保确认对话框的文案清晰明了,告知用户未保存的更改将丢失,并提供明确的“是”和“否”选项。
  4. 性能优化: setDirtyInputs在每次输入变化时都会调用isDeepEqual。对于非常复杂的表单,这可能会有轻微的性能开销。可以考虑使用debounce或throttle来限制其调用频率,或者只在blur事件时检查。
  5. 全局处理: 如果应用中有多个页面需要此功能,可以考虑将useNavigationObserver和AlertModal封装成一个更高级别的组件或上下文提供者,以便在整个应用中统一管理未保存更改的提示逻辑。
  6. 保存操作: 当用户点击“保存”按钮时,记得调用setIsDirty(false)来清除脏状态,这样在保存后立即跳转就不会触发确认对话框。

总结

通过结合Next.js的路由事件监听机制、自定义React Hook以及Chakra UI的对话框组件,我们成功实现了一个健壮的“未保存更改”提示功能。这个方案不仅提升了用户体验,避免了数据意外丢失,也展示了在Next.js中处理复杂路由拦截场景的有效策略。虽然涉及了一些对Next.js内部机制的巧妙利用,但其提供了一种可行的、相对优雅的解决方案。

相关专题

更多
js正则表达式
js正则表达式

php中文网为大家提供各种js正则表达式语法大全以及各种js正则表达式使用的方法,还有更多js正则表达式的相关文章、相关下载、相关课程,供大家免费下载体验。

510

2023.06.20

js获取当前时间
js获取当前时间

JS全称JavaScript,是一种具有函数优先的轻量级,解释型或即时编译型的编程语言;它是一种属于网络的高级脚本语言,主要用于Web,常用来为网页添加各式各样的动态功能。js怎么获取当前时间呢?php中文网给大家带来了相关的教程以及文章,欢迎大家前来学习阅读。

244

2023.07.28

js 字符串转数组
js 字符串转数组

js字符串转数组的方法:1、使用“split()”方法;2、使用“Array.from()”方法;3、使用for循环遍历;4、使用“Array.split()”方法。本专题为大家提供js字符串转数组的相关的文章、下载、课程内容,供大家免费下载体验。

254

2023.08.03

js是什么意思
js是什么意思

JS是JavaScript的缩写,它是一种广泛应用于网页开发的脚本语言。JavaScript是一种解释性的、基于对象和事件驱动的编程语言,通常用于为网页增加交互性和动态性。它可以在网页上实现复杂的功能和效果,如表单验证、页面元素操作、动画效果、数据交互等。

5270

2023.08.17

js删除节点的方法
js删除节点的方法

js删除节点的方法有:1、removeChild()方法,用于从父节点中移除指定的子节点,它需要两个参数,第一个参数是要删除的子节点,第二个参数是父节点;2、parentNode.removeChild()方法,可以直接通过父节点调用来删除子节点;3、remove()方法,可以直接删除节点,而无需指定父节点;4、innerHTML属性,用于删除节点的内容。

477

2023.09.01

js截取字符串的方法
js截取字符串的方法

js截取字符串的方法有substring()方法、substr()方法、slice()方法、split()方法和slice()方法。本专题为大家提供字符串相关的文章、下载、课程内容,供大家免费下载体验。

206

2023.09.04

Js中concat和push的区别
Js中concat和push的区别

Js中concat和push的区别:1、concat用于将两个或多个数组合并成一个新数组,并返回这个新数组,而push用于向数组的末尾添加一个或多个元素,并返回修改后的数组的新长度;2、concat不会修改原始数组,是创建新的数组,而push会修改原数组,将新元素添加到原数组的末尾等等。本专题为大家提供concat和push相关的文章、下载、课程内容,供大家免费下载体验。

217

2023.09.14

js截取字符串的方法介绍
js截取字符串的方法介绍

JavaScript字符串截取方法,包括substring、slice、substr、charAt和split方法。这些方法可以根据具体需求,灵活地截取字符串的不同部分。在实际开发中,根据具体情况选择合适的方法进行字符串截取,能够提高代码的效率和可读性 。

218

2023.09.21

Java 桌面应用开发(JavaFX 实战)
Java 桌面应用开发(JavaFX 实战)

本专题系统讲解 Java 在桌面应用开发领域的实战应用,重点围绕 JavaFX 框架,涵盖界面布局、控件使用、事件处理、FXML、样式美化(CSS)、多线程与UI响应优化,以及桌面应用的打包与发布。通过完整示例项目,帮助学习者掌握 使用 Java 构建现代化、跨平台桌面应用程序的核心能力。

36

2026.01.14

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
React 教程
React 教程

共58课时 | 3.6万人学习

国外Web开发全栈课程全集
国外Web开发全栈课程全集

共12课时 | 1.0万人学习

React核心原理新老生命周期精讲
React核心原理新老生命周期精讲

共12课时 | 1万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号