
本文详细介绍了在Next.js应用中,如何利用自定义React Hook和Chakra UI的AlertDialog组件,实现用户在离开带有未保存更改的页面时,弹出确认对话框的功能。通过巧妙地拦截Next.js路由事件并管理页面状态,确保用户在数据丢失前得到提示,并可选择取消跳转或继续导航至目标路由。
在现代Web应用中,提供良好的用户体验至关重要。其中一个常见场景是,当用户在表单中输入了数据但尚未保存时,意外点击其他链接或尝试返回上一页,导致数据丢失。为了避免这种情况,我们通常需要一个机制来拦截页面跳转,并弹出一个确认对话框,询问用户是否要放弃未保存的更改。本教程将深入探讨如何在Next.js应用中结合Chakra UI实现这一功能。
在React应用中,传统的浏览器API如window.onbeforeunload可以用于在用户关闭页面或刷新时触发确认。然而,对于单页应用(SPA)如Next.js,内部路由跳转并不会触发onbeforeunload。Next.js提供了router.events来监听路由生命周期事件,其中routeChangeStart是拦截路由跳转的关键。
直接在routeChangeStart事件处理器中调用event.preventDefault()并不能完全阻止Next.js的路由跳转。Next.js的内部路由机制更为复杂,它会继续尝试完成导航。因此,我们需要一个更巧妙的方法来“欺骗”Next.js,使其认为路由跳转失败,从而停止导航过程。
为了优雅地解决Next.js的路由拦截问题,我们设计一个名为useNavigationObserver的自定义Hook。这个Hook将负责监听路由变化、阻止导航、显示确认对话框,并在用户确认后恢复导航。
该Hook的核心思想是:
// 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 };关键点解析:
现在,我们将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;集成关键点:
通过结合Next.js的路由事件监听机制、自定义React Hook以及Chakra UI的对话框组件,我们成功实现了一个健壮的“未保存更改”提示功能。这个方案不仅提升了用户体验,避免了数据意外丢失,也展示了在Next.js中处理复杂路由拦截场景的有效策略。虽然涉及了一些对Next.js内部机制的巧妙利用,但其提供了一种可行的、相对优雅的解决方案。
以上就是Next.js与Chakra UI:实现页面跳转前未保存更改确认对话框的详细内容,更多请关注php中文网其它相关文章!
每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号