
本文详细介绍了在 Next.js 应用中,如何结合 Chakra UI 实现用户离页时未保存修改的提示功能。通过自定义 useNavigationObserver Hook,巧妙地拦截 Next.js 路由跳转事件,阻止默认导航行为,并提供弹窗询问用户是否继续。用户确认后,再手动导航至目标页面,确保数据完整性与用户体验。
在现代 Web 应用中,当用户在表单页面进行编辑但尚未保存时,如果尝试离开当前页面(例如点击导航链接或使用浏览器返回按钮),通常需要一个提示来防止数据丢失。在 Next.js 应用中,虽然浏览器提供了 beforeunload 事件来处理页面卸载,但它无法直接控制 Next.js 的客户端路由跳转。Next.js 的 router.events.on('routeChangeStart') 事件可以监听路由变化,但默认情况下,它并不能直接阻止路由的完成,这导致即使检测到未保存的更改并尝试打开弹窗,路由仍可能继续,弹窗也无法正常显示。
核心挑战在于:
为了解决上述问题,我们可以创建一个自定义的 useNavigationObserver Hook。这个 Hook 的核心思想是利用 Next.js 路由事件的特性,通过“抛出假错误”的方式来中断路由跳转,并在用户确认后,手动恢复导航。
import { useRouter } from "next/router";
import { useCallback, useEffect, useRef } from "react";
// 定义一个独特的错误消息,用于识别并阻止路由错误
const errorMessage = "Please ignore this error.";
// 抛出一个假错误以欺骗 Next.js 路由
const throwFakeErrorToFoolNextRouter = () => {
// eslint-disable-next-line no-throw-literal
throw errorMessage;
};
// 拦截并阻止 Next.js 内部的 PromiseRejectionEvent,防止假错误被报告
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(() => {
// 触发 'routeChangeError' 事件,Next.js 会认为路由失败
router.events.emit("routeChangeError", "", "", { shallow: false });
throwFakeErrorToFoolNextRouter(); // 抛出假错误以中断 Promise 链
}, [router]);
useEffect(() => {
navigationConfirmed.current = false; // 每次组件挂载或依赖更新时重置确认状态
const onRouteChange = (url: string) => {
// 如果 URL 已经改变,但我们想阻止导航,需要将浏览器历史状态推回当前路径
// 这是因为在 routeChangeStart 发生时,浏览器地址栏可能已经更新了
if (currentPath !== url) {
window.history.pushState(null, "", router.basePath + currentPath);
}
// 只有当满足以下条件时才阻止导航:
// 1. shouldStopNavigation 为 true (即有未保存的更改)
// 2. 目标 URL 与当前 URL 不同
// 3. 用户尚未确认导航
if (
shouldStopNavigation &&
url !== currentPath &&
!navigationConfirmed.current
) {
nextPath.current = url.replace(router.basePath, ""); // 存储目标路径
onNavigate(); // 调用传入的回调函数,通常用于打开弹窗
killRouterEvent(); // 阻止路由继续
}
};
router.events.on("routeChangeStart", onRouteChange);
window.addEventListener("unhandledrejection", rejectionHandler); // 监听未处理的 Promise 拒绝
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 };Hook 关键点解释:
throwFakeErrorToFoolNextRouter & rejectionHandler:
window.history.pushState:
nextPath 和 navigationConfirmed:
confirmNavigation:
现在,我们将 useNavigationObserver Hook 集成到 Next.js 组件中,以实现离页提示功能。
import { useState } from "react";
import {
Box,
Grid,
GridItem,
Input,
Flex,
Button,
useColorModeValue,
useDisclosure, // Chakra UI Hook for managing modal state
} from "@chakra-ui/react";
// ... 其他导入,如 PasswordEditor, TopNav, services, showMsg, deep-equal 等
import { useNavigationObserver } from "@/hooks/useNavigationObserver"; // 导入自定义 Hook
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 }; // 初始记录状态,用于比较
const title = type === "new" ? "New Record" : "Edit Record";
const router = useRouter();
const { recordId } = router.query;
const buttonBg = useColorModeValue("#dbdbdb", "#2a2c38");
// 使用 Chakra UI 的 useDisclosure 管理弹窗的打开/关闭状态
const { isOpen, onOpen, onClose } = useDisclosure();
// 使用自定义的 useNavigationObserver 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);
}
};
// 处理输入变化,并更新 isDirty 状态
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setRecordObj((prevState) => ({
...prevState,
[e.target.id]: e.target.value,
}));
setDirtyInputs(); // 每次输入变化后检查脏状态
};
// 处理表单提交
const handleSubmit = () => {
setIsDirty(false); // 提交后重置脏状态
// ... 保存逻辑 ...
if (type === "new") {
postMethod(`/api/user/${user.id}/records`, {
...recordObj,
password,
})
.then(() => router.push("/"))
.then(() => showMsg("Record saved", { type: "success" }))
.catch(() => showMsg("Something went wrong", { type: "error" }));
} else {
updateMethod(`/api/user/${user.id}/records/${recordId}`, {
...recordObj,
password,
})
.then(() => router.push("/"))
.then(() => showMsg("Record updated", { type: "success" }))
.catch(() => showMsg("Something went wrong", { type: "error" }));
}
};
return (
<Box py="60px">
<TopNav title={title} type="backAndTitle" />
<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 password={password} setPassword={setPassword} />
<Box mt="20px">
<Button
type="submit"
w="100%"
background={buttonBg}
_focus={{ background: buttonBg }}
onClick={handleSubmit}
>
Save Record
</Button>
</Box>
{/* Chakra UI 弹窗组件 */}
{/* 假设 AlertModal 是一个自定义组件,内部封装了 Chakra UI 的 AlertDialog */}
<AlertModal
type="leave"
onClose={onClose} // 关闭弹窗的回调
isOpen={isOpen} // 控制弹窗是否显示
callBackAction={navigate} // 用户确认离开时调用 navigate 函数
/>
</Box>
);
};
export default RecordEditing;组件集成关键点:
isDirty 状态管理:
useDisclosure:
useNavigationObserver 的使用:
AlertModal (或 AlertDialog):
通过 useNavigationObserver Hook,我们成功地在 Next.js 应用中实现了一个健壮的离页提示功能,极大地提升了用户体验和数据安全性。这种模式提供了一个可复用的解决方案,可以轻松集成到任何需要此功能的表单组件中。
以上就是Next.js 与 Chakra UI:实现页面未保存修改离页提示与导航控制的详细内容,更多请关注php中文网其它相关文章!
每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号