
本文详细介绍了如何在 Next.js 应用中,结合 Chakra UI 实现一个健壮的页面导航防护机制。当用户在包含未保存更改的表单页面尝试离开时,系统将通过一个自定义 React Hook 拦截路由跳转,并弹出一个 Chakra UI 警告对话框,询问用户是否确认离开。该方案通过巧妙地利用 Next.js 路由事件和浏览器历史 API,确保用户在确认前不会丢失数据,并能准确地导航到其最初选择的路由。
在现代 Web 应用中,用户体验至关重要。当用户在表单中输入数据但尚未保存时,意外的页面导航可能导致数据丢失,从而带来糟糕的用户体验。为了解决这一问题,我们需要一种机制来检测未保存的更改,并在用户尝试离开页面时提供确认提示。本教程将指导您如何在 Next.js 应用中,利用 Chakra UI 的 AlertDialog 组件和自定义 Hook 来实现这一功能。
Next.js 提供了 router.events 来监听路由变化,例如 routeChangeStart。然而,仅仅监听这个事件并不能直接“阻止”路由跳转。当 routeChangeStart 事件触发时,Next.js 已经开始处理导航,并且通常会立即更新浏览器的 URL。为了真正阻止导航并等待用户确认,我们需要更高级的策略。
主要的挑战点在于:
为了优雅地解决上述挑战,我们将创建一个名为 useN*igationObserver 的自定义 React Hook。这个 Hook 将负责监听路由事件、阻止默认导航行为、保存目标路由,并提供一个回调函数来允许组件在用户确认后恢复导航。
AI at Meta
Facebook 旗下的AI研究平台
72
查看详情
import { useRouter } from "next/router";
import { useCallback, useEffect, useRef } from "react";
// 用于欺骗 Next.js 路由的假错误信息
const errorMessage = "Please ignore this error.";
// 抛出假错误以中断 Next.js 路由
const throwFakeErrorToFoolNextRouter = () => {
// eslint-disable-next-line no-throw-literal
throw errorMessage;
};
// 拦截并阻止处理我们的假错误,防止其被报告到控制台
const rejectionHandler = (event: PromiseRejectionEvent) => {
if (event.reason === errorMessage) {
event.preventDefault();
}
};
interface Props {
shouldStopN*igation: boolean; // 是否应该阻止导航
onN*igate: () => void; // 当导航被阻止时调用的回调函数(用于打开对话框)
}
const useN*igationObserver = ({ shouldStopN*igation, onN*igate }: Props) => {
const router = useRouter();
const currentPath = router.asPath; // 当前页面的完整路径
const nextPath = useRef(""); // 存储用户尝试导航到的目标路径
const n*igationConfirmed = useRef(false); // 标记用户是否已确认导航
// 中断路由事件并抛出假错误
const killRouterEvent = useCallback(() => {
// 触发一个路由错误事件,Next.js 会捕获并停止导航
router.events.emit("routeChangeError", "", "", { shallow: false });
throwFakeErrorToFoolNextRouter(); // 抛出错误以强制中断
}, [router]);
useEffect(() => {
// 每次组件重新渲染时重置确认状态
n*igationConfirmed.current = false;
const onRouteChange = (url: string) => {
// 如果当前路径与目标路径不同,且导航未被确认
if (currentPath !== url) {
// 立即将浏览器历史记录的 URL 恢复到当前路径
// 这是为了防止在对话框显示之前,URL 短暂地变为目标路径
window.history.pushState(null, "", router.basePath + currentPath);
}
// 如果有未保存的更改 (shouldStopN*igation 为 true)
// 且目标路径与当前路径不同,且导航未被用户确认
if (
shouldStopN*igation &&
url !== currentPath &&
!n*igationConfirmed.current
) {
nextPath.current = url.replace(router.basePath, ""); // 存储目标路径
onN*igate(); // 调用回调函数,通常用于打开确认对话框
killRouterEvent(); // 阻止 Next.js 导航
}
};
// 监听路由开始变化事件
router.events.on("routeChangeStart", onRouteChange);
// 监听未处理的 Promise 拒绝,用于捕获并阻止我们的假错误被报告
window.addEventListener("unhandledrejection", rejectionHandler);
return () => {
// 组件卸载时移除事件监听器
router.events.off("routeChangeStart", onRouteChange);
window.removeEventListener("unhandledrejection", rejectionHandler);
};
}, [
currentPath,
killRouterEvent,
onN*igate,
router.basePath,
router.events,
shouldStopN*igation,
]);
// 用户确认导航后调用此函数
const confirmN*igation = () => {
n*igationConfirmed.current = true; // 标记导航已确认
router.push(nextPath.current); // 手动导航到之前存储的目标路径
};
return confirmN*igation;
};
export { useN*igationObserver };现在,我们将在一个 Next.js 页面组件中集成 useN*igationObserver Hook 和 Chakra UI 的 AlertDialog。
假设我们有一个 RecordEditing 组件,用于编辑记录。
import { useState, useEffect } from "react";
import {
Box,
Grid,
GridItem,
Input,
Flex,
Button,
useColorModeValue,
useDisclosure,
AlertDialog,
AlertDialogBody,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogContent,
AlertDialogOverlay,
} from "@chakra-ui/react";
import { useRouter } from "next/router";
import isDeepEqual from "deep-equal"; // 用于深度比较对象
import { useN*igationObserver } from "@/hooks/useN*igationObserver"; // 导入自定义 Hook
// 假设的类型定义和辅助函数
interface IRecordEditData {
title: string;
url: string;
username: string;
password?: string;
}
interface IRecordEditingProps {
type: "new" | "edit";
record: IRecordEditData;
user: { id: string };
}
const postMethod = async (url: string, data: any) => { /* ... */ };
const updateMethod = async (url: string, data: any) => { /* ... */ };
const showMsg = (message: string, options: { type: string }) => { /* ... */ };
const TopN* = ({ title, type }: { title: string; type: string }) => { /* ... */ return <Box>{title}</Box>; };
const PasswordEditor = ({ password, setPassword }: { password: string; setPassword: (p: string) => void }) => { /* ... */ return <Input value={password} onChange={(e) => setPassword(e.target.value)} placeholder="Password" />; };
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 Hook 管理对话框状态
const { isOpen, onOpen, onClose } = useDisclosure();
// 使用自定义导航观察者 Hook
const n*igate = useN*igationObserver({
shouldStopN*igation: isDirty, // 只有当 isDirty 为 true 时才阻止导航
onN*igate: () => 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,
}));
};
// 在 recordObj 或 password 变化时检查脏状态
useEffect(() => {
setDirtyInputs();
}, [recordObj, password]);
// 处理表单提交
const handleSubmit = async () => {
setIsDirty(false); // 提交后清除脏状态
try {
if (type === "new") {
await postMethod(`/api/user/${user.id}/records`, {
...recordObj,
password,
});
showMsg("Record s*ed", { type: "success" });
} else {
await updateMethod(`/api/user/${user.id}/records/${recordId}`, {
...recordObj,
password,
});
showMsg("Record updated", { type: "success" });
}
router.push("/"); // 保存成功后导航到主页
} catch (error) {
showMsg("Something went wrong", { type: "error" });
}
};
return (
<Box py="60px">
<TopN* 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>
<GridItem w="100%" h="10">
<Flex align="center" h="100%">
Url
</Flex>
</GridItem>
<GridItem w="100%" h="10">
<Input
id="url"
value={recordObj.url}
placeholder="Website url (optional)"
onChange={handleInputChange}
_focusVisible={{ border: "2px solid", borderColor: "teal.200" }}
/>
</GridItem>
<GridItem w="100%" h="10">
<Flex align="center" h="100%">
Username
</Flex>
</GridItem>
<GridItem w="100%" h="10">
<Input
id="username"
value={recordObj.username}
placeholder="Username or email"
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}
>
S*e Record
</Button>
</Box>
{/* Chakra UI AlertDialog for uns*ed changes */}
<AlertDialog
motionPreset="slideInBottom"
leastDestructiveRef={undefined} // 可以指向 "No" 按钮的 ref
onClose={onClose}
isOpen={isOpen}
isCentered
>
<AlertDialogOverlay />
<AlertDialogContent>
<AlertDialogHeader>
您确定要离开此页面吗?
</AlertDialogHeader>
<AlertDialogBody>
您有未保存的更改。如果您离开此页面,所有未保存的更改都将丢失!
</AlertDialogBody>
<AlertDialogFooter>
<Button onClick={onClose}>
否 (留在当前页)
</Button>
<Button
onClick={() => {
onClose(); // 关闭对话框
n*igate(); // 调用 Hook 返回的函数,继续导航到目标页
}}
colorScheme="red"
ml={3}
>
是 (离开此页)
</Button>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</Box>
);
};
export default RecordEditing;通过结合 Next.js 的路由事件、浏览器历史 API 以及 Chakra UI 的模态框组件,我们成功构建了一个强大且用户友好的导航防护系统。useN*igationObserver Hook 提供了一个可复用的解决方案,能够优雅地处理未保存更改的场景,从而显著提升 Next.js 应用的用户体验。这种模式不仅适用于表单页面,也可以扩展到任何需要用户确认才能离开的复杂交互场景。
以上就是Next.js 与 Chakra UI:构建优雅的未保存更改导航防护的详细内容,更多请关注其它相关文章!
# 关键词排名薇薪
# 跳转
# 抛出
# 这是
# 它会
# 这一
# 并在
# 网站建设推广刘江
# 沧州网站建设费用报价
# 回调
# 游戏项目营销推广方案
# 邢台网站建设与运营
# 长岛个性化网站优化
# 大学生营销推广方案范文
# 外贸营销推广招聘要求怎么写
# 作图网站建设素材库
# seo和百度区别
# win
# word
# html
# js
# go
# 浏览器
# 回调函数
# ai
# 路由
# react
# 数据丢失
# 表单提交
# red
# gate
# 对话框
# 表单
# 自定义
相关栏目:
【
Google疑问12 】
【
Facebook疑问10 】
【
优化推广96088 】
【
技术知识133117 】
【
IDC资讯59369 】
【
网络运营7196 】
【
IT资讯61894 】
相关推荐:
《一起考教师》账号注销方法
《绝区零》2.3前瞻|直播|内容介绍
Win11怎么开启HDR_Windows 11显示器画质增强设置
Symfony路由参数转换器:实体存在性验证与错误处理策略
邮编号码查询app有哪些_邮编号码查询推荐app及使用体验
汽水音乐网页版登录 汽水音乐网页端官方入口
win11关机几秒又自己开机 Win11关机自动重启问题修复
全球各国上班时间表外贸邮件时间
《海豚家》注销账号方法
邦丰播放器频道搜索设置
在J*a里什么是行为抽象_抽象行为对代码复用的提升作用
我居然低估了 DeepSeek,这次更新它做到了这些!
家里的小飞虫总是不断,用什么方法可以彻底根除?
12306夜间购票失败? | 查看官方公布的暂停服务公告与应对方案
铁路12306买票怎么选双人铺 铁路12306卧铺分配规则说明
《淘票票》添加到苹果钱包教程
Python中处理嵌套字典与列表的数据提取与过滤教程
微信客户端如何找回密码_微信客户端忘记密码找回方法
C++怎么实现一个红黑树_C++高级数据结构与平衡二叉搜索树
《大周列国志》皇帝律令功能介绍
word表格如何按某一列内容进行排序_Word表格按列排序方法
123网页端官方登录页 123邮箱网页版即时通讯服务
Sublime怎么自动添加CSS前缀_Sublime安装Autoprefixer插件
《米姆米姆哈》米姆获取及技能攻略
J*a列表元素格式化输出教程
动漫岛在线动漫网 动漫岛动漫在线观看官方入口
Win10如何关闭操作中心通知 Win10免打扰设置全攻略【清爽】
视频转蓝光m2ts格式
《海底捞》点外卖方法
windows10怎么关闭自动安装应用_windows10禁止推广应用下载
mysql如何回滚事务_mysql ROLLBACK事务回滚方法
解决C#跨线程访问XML对象的异常 安全的并发XML处理模式
铁拳8在线玩 铁拳8在线秒玩入口
Flask 应用中图片动态更新与上传:实现客户端定时刷新与服务器端文件管理
企查查官网和爱企查 企查查企业查询官网入口
免费占卜在线神算_免费占卜手机神算
Three.js中动态更换3D模型纹理的教程
《随手记》关闭首页消息推送方法
汽水音乐官网网页版入口 汽水音乐官网网页版在线入口
Google Drive API服务器端访问指南:服务账户认证详解
263企业邮箱如何设置邮件转发功能
Sublime怎么格式化HTML代码_Sublime前端代码美化插件使用指南
店铺如何做视频号推广?做视频号推广有用吗?
Python类装饰器动态修改方法时的类型提示:Mypy插件实现精确静态分析
TikTok网页版实时观看入口 TikTok网页版短视频在线浏览
小红书网页版首页入口 小红书网页版电脑端官方登录链接
POKI小游戏在线免费入口链接 POKI小游戏无下载秒玩玩
《幻兽帕鲁》手游帕鲁捕捉技巧分享
蛙漫2(台版)正版官网 2025免费网页版分享
win11怎么启用或禁用休眠 Win11 powercfg命令管理休眠文件【技巧】
2025-11-18
运城市盐湖区信雨科技有限公司是一家深耕海外推广领域十年的专业服务商,作为谷歌推广与Facebook广告全球合作伙伴,聚焦外贸企业出海痛点,以数字化营销为核心,提供一站式海外营销解决方案。公司凭借十年行业沉淀与平台官方资源加持,打破传统外贸获客壁垒,助力企业高效开拓全球市场,成为中小企业出海的可靠合作伙伴。