首页 > web前端 > js教程 > 正文

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

霞舞
发布: 2025-11-18 19:19:13
原创
768人浏览过

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 };
登录后复制

关键点解析:

居然设计家
居然设计家

居然之家和阿里巴巴共同打造的家居家装AI设计平台

居然设计家 199
查看详情 居然设计家
  • 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<IRecordEditingProps> = ({
  type,
  record,
  user,
}) => {
  const [recordObj, setRecordObj] = useState<IRecordEditData>(record);
  const [password, setPassword] = useState<string>(record.password || "");
  const [isDirty, setIsDirty] = useState<boolean>(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<HTMLInputElement>) => {
    setRecordObj((prevState) => ({
      ...prevState,
      [e.target.id]: e.target.value,
    }));
    setDirtyInputs(); // 每次输入变化后检查脏状态
  };

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

  // 在组件渲染时,如果 AlertModal 是一个独立的组件,需要确保它能接收到正确的props
  // 这里假设 AlertModal 内部处理了 AlertDialog 的渲染逻辑
  // 并且通过 callBackAction 属性接收 navigate 函数
  return (
    <Box py="60px">
      {/* ... 页面内容,例如输入框和保存按钮 */}
      <Box>
        <Grid gridTemplateColumns="3fr 6fr" gap="10px" py="10px">
          <GridItem w="100%" h="10">
            <Flex align="center" h="100%">
              Title
            </Flex>
          </GridItem>
          <GridItem w="100%" h="10">
            <Input
              id="title"
              value={recordObj.title}
              placeholder="Record title"
              onChange={handleInputChange}
              _focusVisible={{ border: "2px solid", borderColor: "teal.200" }}
            />
          </GridItem>
          {/* 其他输入字段 */}
        </Grid>
      </Box>
      {/* ... PasswordEditor 组件 */}
      <Box mt="20px">
        <Button
          type="submit"
          w="100%"
          onClick={handleSubmit}
        >
          Save Record
        </Button>
      </Box>

      {/* 确认离开对话框 */}
      <AlertModal
        type="leave"
        onClose={onClose}
        isOpen={isOpen}
        callBackAction={navigate} // 当用户确认离开时,调用 navigate 函数继续跳转
      />
    </Box>
  );
};

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<AlertModalProps> = ({
  isOpen,
  onClose,
  type,
  callBackAction,
}) => {
  const cancelRef = useRef<HTMLButtonElement>(null);

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

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

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

  return (
    <AlertDialog
      motionPreset="slideInBottom"
      leastDestructiveRef={cancelRef}
      onClose={onClose}
      isOpen={isOpen}
      isCentered
    >
      <AlertDialogOverlay />
      <AlertDialogContent>
        <AlertDialogHeader>{getHeader()}</AlertDialogHeader>
        <AlertDialogBody>{getBody()}</AlertDialogBody>
        <AlertDialogFooter>
          <Button ref={cancelRef} onClick={onClose}>
            否
          </Button>
          <Button colorScheme="red" onClick={handleConfirm} ml={3}>
            是
          </Button>
        </AlertDialogFooter>
      </AlertDialogContent>
    </AlertDialog>
  );
};

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内部机制的巧妙利用,但其提供了一种可行的、相对优雅的解决方案。

以上就是Next.js与Chakra UI:实现页面跳转前未保存更改确认对话框的详细内容,更多请关注php中文网其它相关文章!

最佳 Windows 性能的顶级免费优化软件
最佳 Windows 性能的顶级免费优化软件

每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。

下载
来源:php中文网
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn
最新问题
开源免费商场系统广告
热门教程
更多>
最新下载
更多>
网站特效
网站源码
网站素材
前端模板
关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新 English
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送
PHP中文网APP
随时随地碎片化学习

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