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

React中Popstate事件在useEffect清理前未触发的问题排查

解决React中popstate事件监听器在页面导航时先被移除无法触发的问题

问题描述

我需要实现用户离开页面时的「确认离开」提示(避免未保存更改丢失),简化后的Hook示例如下:

import { useEffect } from "react";

const useUnsavedChangesAlert = () => {
  useEffect(() => {
    const handleRouteChange = (event) => {
      console.log("IN HANDLE ROUTE CHANGE!!");
    };

    console.log("ADD POPSTATE EVENT LISTENER!!");
    window.addEventListener("popstate", handleRouteChange);

    return () => {
      console.log("REMOVE POPSTATE EVENT LISTENER!!");
      window.removeEventListener("popstate", handleRouteChange);
    };
  }, []);
};

export default useUnsavedChangesAlert;

预期点击浏览器返回按钮时,popstate事件先触发handleRouteChange,再执行useEffect清理函数移除监听器,但实际handleRouteChange的日志完全没输出。

在create-react-app中的复现代码:

import React, { useEffect } from "react";

export default function Home() {
  useEffect(() => {
    const handleRouteChange = (event) => {
      console.log("IN HANDLE ROUTE CHANGE!!");
      alert("Are you sure you want to leave this page?");
    };

    console.log("ADD POPSTATE EVENT LISTENER!!");
    window.addEventListener("popstate", handleRouteChange);

    return () => {
      console.log("REMOVE POPSTATE EVENT LISTENER!!");
      window.removeEventListener("popstate", handleRouteChange);
    };
  }, []);
  return <h1>Home</h1>;
}

该问题在Next.js 14和create-react-app中均存在,注释掉removeEventListener后,handleRouteChange可正常执行。推测是页面导航导致组件卸载,useEffect清理函数先移除了popstate监听器,导致事件无法触发。需要解决让事件先触发再执行清理的问题。

解决方案

方法一:使用beforeunload事件替代(推荐)

对于浏览器前进/后退、关闭标签页这类页面卸载场景,原生beforeunload事件是更适配的选择——它会在页面即将卸载前优先触发,不会被组件清理函数抢先执行。修改后的Hook如下:

import { useEffect, useState } from "react";

const useUnsavedChangesAlert = () => {
  // 用状态标记是否存在未保存更改
  const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);

  useEffect(() => {
    const handleBeforeUnload = (event) => {
      if (hasUnsavedChanges) {
        // 部分浏览器需要通过preventDefault和returnValue触发提示
        event.preventDefault();
        event.returnValue = "您有未保存的更改,确定要离开吗?";
        return "您有未保存的更改,确定要离开吗?";
      }
    };

    window.addEventListener("beforeunload", handleBeforeUnload);

    return () => {
      window.removeEventListener("beforeunload", handleBeforeUnload);
    };
  }, [hasUnsavedChanges]);

  // 向外暴露修改未保存状态的方法
  return { setHasUnsavedChanges };
};

export default useUnsavedChangesAlert;

使用时,在表单输入、数据修改等操作中调用setHasUnsavedChanges(true)即可标记未保存状态,触发离开确认提示。

方法二:针对单页应用路由跳转,使用框架路由监听API

在React/Next.js这类单页应用中,路由跳转由框架内部处理,直接监听popstate容易和框架逻辑冲突。可以改用框架提供的路由监听能力:

Next.js(App Router)方案

结合usePathname和useRouter拦截路由跳转,同时处理浏览器前进后退:

"use client";
import { useEffect, useState } from "react";
import { usePathname, useRouter } from "next/navigation";

const useUnsavedChangesAlert = () => {
  const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
  const pathname = usePathname();
  const router = useRouter();

  useEffect(() => {
    // 处理浏览器前进后退
    const handlePopState = () => {
      if (hasUnsavedChanges) {
        const confirmLeave = window.confirm("您有未保存的更改,确定要离开吗?");
        if (!confirmLeave) {
          // 用户取消时,回退到当前路径
          history.pushState(null, "", pathname);
        }
      }
    };

    // 拦截编程式路由跳转
    const originalPush = router.push;
    router.push = (...args) => {
      if (hasUnsavedChanges) {
        const confirmLeave = window.confirm("您有未保存的更改,确定要离开吗?");
        if (!confirmLeave) return;
      }
      return originalPush.apply(router, args);
    };

    window.addEventListener("popstate", handlePopState);

    return () => {
      // 恢复原路由方法并移除监听器
      router.push = originalPush;
      window.removeEventListener("popstate", handlePopState);
    };
  }, [hasUnsavedChanges, pathname, router]);

  return { setHasUnsavedChanges };
};

export default useUnsavedChangesAlert;

React Router方案

通过替换navigate方法拦截编程式跳转,同时监听popstate处理浏览器导航:

import { useEffect, useState } from "react";
import { useNavigate, useLocation } from "react-router-dom";

const useUnsavedChangesAlert = () => {
  const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
  const navigate = useNavigate();
  const location = useLocation();

  useEffect(() => {
    // 处理浏览器前进后退
    const handlePopState = () => {
      if (hasUnsavedChanges) {
        const confirmLeave = window.confirm("您有未保存的更改,确定要离开吗?");
        if (!confirmLeave) {
          history.pushState(null, "", location.pathname);
        }
      }
    };

    // 拦截编程式路由跳转
    const originalNavigate = navigate;
    const guardedNavigate = (...args) => {
      if (hasUnsavedChanges) {
        const confirmLeave = window.confirm("您有未保存的更改,确定要离开吗?");
        if (!confirmLeave) return;
      }
      return originalNavigate.apply(null, args);
    };

    Object.assign(navigate, guardedNavigate);
    window.addEventListener("popstate", handlePopState);

    return () => {
      Object.assign(navigate, originalNavigate);
      window.removeEventListener("popstate", handlePopState);
    };
  }, [hasUnsavedChanges, navigate, location]);

  return { setHasUnsavedChanges };
};

export default useUnsavedChangesAlert;

方法三:延迟清理监听器(应急方案,不推荐)

如果必须使用popstate,可以通过setTimeout延迟清理函数执行,确保事件先触发:

import { useEffect } from "react";

const useUnsavedChangesAlert = () => {
  useEffect(() => {
    const handleRouteChange = (event) => {
      console.log("IN HANDLE ROUTE CHANGE!!");
      const confirmLeave = window.confirm("您有未保存的更改,确定要离开吗?");
      if (confirmLeave) {
        // 用户确认后手动移除监听器
        window.removeEventListener("popstate", handleRouteChange);
      } else {
        // 用户取消,恢复历史状态
        history.pushState(null, "", window.location.pathname);
      }
    };

    console.log("ADD POPSTATE EVENT LISTENER!!");
    window.addEventListener("popstate", handleRouteChange);

    return () => {
      // 延迟100ms执行清理,确保事件先触发
      setTimeout(() => {
        console.log("REMOVE POPSTATE EVENT LISTENER!!");
        window.removeEventListener("popstate", handleRouteChange);
      }, 100);
    };
  }, []);
};

export default useUnsavedChangesAlert;

注意:这种方法依赖定时器延迟,存在时序兼容性问题,仅适合临时应急使用。

内容的提问来源于stack exchange,提问作者Luke Sharon

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.22 15:54:53