You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

NextJS+ChakraUI实现未保存内容时离开页面弹出警告对话框

问题解决方案

问题1:对话框无法正常弹出

routeChangeStart触发后Next.js会默认执行页面跳转,导致对话框刚打开就被覆盖。需要检测到未保存更改时阻止默认导航,再弹出对话框。

问题2:确认后跳转到目标路由

触发导航事件时记录用户原本要跳转的目标路由,点击"YES"时手动执行跳转。

修改后的完整代码

import {
  Box,
  Grid,
  GridItem,
  Input,
  Flex,
  Button,
  useColorModeValue,
} from "@chakra-ui/react";
import {
  AlertDialog,
  AlertDialogBody,
  AlertDialogFooter,
  AlertDialogHeader,
  AlertDialogContent,
  AlertDialogOverlay,
  useDisclosure,
} from "@chakra-ui/react";
import PasswordEditor from "@/components/PasswordEditor";
import { useRouter } from "next/router";
import { postMethod, updateMethod } from "@/helpers/services";
import TopNav from "./TopNav";
import showMsg from "@/helpers/showMsg";
import { IRecordEditingProps, IRecordEditData } from "@/types/components";
import deepEqual from "deep-equal";
import { useState, useEffect, useRef } from "react";

const RecordEditing: React.FC<IRecordEditingProps> = ({
  type,
  record,
  user,
}) => {
  const [recordObj, setRecordObj] = useState<IRecordEditData>(record);
  const [password, setPassword] = useState<string>(record.password);
  const [targetRoute, setTargetRoute] = useState<string | null>(null);
  const title = type === "new" ? "New Record" : "Edit Record";
  const router = useRouter();
  const { recordId } = router.query;
  const { isOpen, onOpen, onClose } = useDisclosure();
  const cancelRef = useRef<HTMLButtonElement>(null);
  const buttonBg = useColorModeValue("#dbdbdb", "#2a2c38");

  const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    setRecordObj((prevState) => ({
      ...prevState,
      [e.target.id]: e.target.value,
    }));
  };

  const handleSubmit = () => {
    if (type === "new") {
      postMethod(`/api/user/${user.id}/records`, {
        ...recordObj,
        password,
      })
        .then(() => router.push("/"))
        .catch(() => showMsg("Something went wrong", { type: "error" }));
    } else {
      updateMethod(`/api/user/${user.id}/records/${recordId}`, {
        ...recordObj,
        password,
      })
        .then(() => router.push("/"))
        .catch(() => showMsg("Something went wrong", { type: "error" }));
    }
  };

  useEffect(() => {
    const checkForChanges = (url: string) => {
      // 同时检查recordObj和password的变化
      const hasUnsavedChanges = !deepEqual(recordObj, record) || password !== record.password;
      
      if (hasUnsavedChanges) {
        // 阻止默认导航
        router.events.emit("routeChangeError");
        // 记录目标路由
        setTargetRoute(url);
        // 打开对话框
        onOpen();
        console.log("Warning for changes!!!!!!!!!!");
      }
    };

    router.events.on("routeChangeStart", checkForChanges);

    return () => {
      router.events.off("routeChangeStart", checkForChanges);
    };
  }, [recordObj, record, password, router, onOpen]);

  // 点击YES后的跳转逻辑
  const handleConfirmLeave = () => {
    if (targetRoute) {
      router.push(targetRoute);
    }
    onClose();
  };

  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>
          <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}
        >
          Save Record
        </Button>
      </Box>
      <AlertDialog
        motionPreset="slideInBottom"
        leastDestructiveRef={cancelRef}
        onClose={onClose}
        isOpen={isOpen}
        isCentered
      >
        <AlertDialogOverlay />

        <AlertDialogContent>
          <AlertDialogHeader>
            Are you sure you want to leave this page?
          </AlertDialogHeader>
          <AlertDialogBody>
            You have unsaved changes. Are you sure you want to leave this page?
            Unsaved changes will be lost!
          </AlertDialogBody>
          <AlertDialogFooter>
            <Button ref={cancelRef} onClick={onClose}>
              No
            </Button>
            <Button
              onClick={handleConfirmLeave}
              colorScheme="red"
              ml={3}
            >
              Yes
            </Button>
          </AlertDialogFooter>
        </AlertDialogContent>
      </AlertDialog>
    </Box>
  );
};

export default RecordEditing;

关键修改点说明

  1. 阻止默认导航:调用router.events.emit("routeChangeError")取消默认跳转,确保对话框正常显示。
  2. 记录目标路由:新增targetRoute状态存储跳转URL,点击"YES"时通过router.push(targetRoute)完成跳转。
  3. 完善变更检测:将password加入变更判断,覆盖密码修改的未保存场景。
  4. 更新依赖数组:确保事件监听能正确响应状态变化。

内容的提问来源于stack exchange,提问作者Stef-Lev

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.18 11:37:11