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

Next.js隔离组件按钮点击触发确认弹窗的优化方案咨询

优化Next.js中未保存表单变更的拦截方案

我在React(Next.js)项目里维护了一个全局标记formHasChange,用来阻止用户在表单存在未保存变更时跳转路由。现在有6个独立组件里的按钮点击操作也需要做同样的拦截——有未保存变更时弹出确认框,确认后才执行原操作。

目前我的实现方式是:

  • 封装EventWrapper组件,用来包裹按钮,检查hasChange状态,弹出确认后再执行点击事件
  • 用useUnsaveChangeConfirm钩子处理路由跳转的拦截

想请教有没有更优的实现方案?


当前实现代码

1. EventWrapper 包装组件

import React, { Children, isValidElement, cloneElement, SyntheticEvent } from 'react';

const EventWrapper = ({ children, onClick, hasChange }) => {
  function areYouSure(event) {
    if (hasChange) {
      return confirm('Are you sure want to discard changes ?') ? onClick(event) : event.preventDefault();
    } else {
      onClick(event);
    }
  }

  const childrenWithProps = Children.map(children, (child) => {
    if (isValidElement(child)) {
      return cloneElement(child, { onClick: (event: SyntheticEvent) => areYouSure(event) });
    }
    return child;
  });

  return <> {childrenWithProps} </>;
};

export default EventWrapper;

2. useUnsaveChangeConfirm 路由拦截钩子

import Router from 'next/router';
import { useEffect } from 'react';
import { useBeforeUnload } from 'react-use';
import { useSettings } from './useSettings';
import themeConfig from 'src/config/themeConfig';
import nProgress from 'nprogress';

const useUnsaveChangeConfirm = (isConfirm = true, message = 'Are you sure want to leave this page?') => {
  const { settings, saveSettings } = useSettings();

  useBeforeUnload(isConfirm, message);

  useEffect(() => {
    const handler = () => {
      if (isConfirm && !window.confirm(message)) {
        if (settings.routingLoader) {
          nProgress.done();
        }
        throw 'Route Canceled due to unsaved changes';
      }
    };

    Router.events.on('routeChangeStart', handler);

    return () => {
      Router.events.off('routeChangeStart', handler);
      saveSettings({ ...settings, routingLoader: themeConfig.routingLoader });
    };
  }, [isConfirm, message]);
};

export default useUnsaveChangeConfirm;

优化方案建议

1. 统一拦截逻辑到单个自定义钩子

把按钮点击和路由跳转的拦截逻辑合并到一个钩子中,避免重复代码,同时简化调用方式:

import { useCallback, useEffect } from 'react';
import Router from 'next/router';
import { useBeforeUnload } from 'react-use';
import { useSettings } from './useSettings';
import themeConfig from 'src/config/themeConfig';
import nProgress from 'nprogress';

const useUnsavedChangesGuard = (hasUnsavedChanges, confirmMessage = 'Are you sure you want to discard unsaved changes?') => {
  const { settings, saveSettings } = useSettings();

  // 拦截页面刷新/关闭
  useBeforeUnload(hasUnsavedChanges, confirmMessage);

  // 拦截路由跳转
  useEffect(() => {
    const handleRouteChange = () => {
      if (hasUnsavedChanges && !window.confirm(confirmMessage)) {
        if (settings.routingLoader) {
          nProgress.done();
        }
        throw 'Route Canceled due to unsaved changes';
      }
    };

    if (hasUnsavedChanges) {
      Router.events.on('routeChangeStart', handleRouteChange);
    } else {
      Router.events.off('routeChangeStart', handleRouteChange);
    }

    return () => {
      Router.events.off('routeChangeStart', handleRouteChange);
      saveSettings({ ...settings, routingLoader: themeConfig.routingLoader });
    };
  }, [hasUnsavedChanges, confirmMessage]);

  // 生成带拦截的点击事件处理函数
  const createGuardedOnClick = useCallback((originalOnClick) => {
    return (event) => {
      if (hasUnsavedChanges) {
        if (window.confirm(confirmMessage)) {
          originalOnClick?.(event);
        } else {
          event.preventDefault();
        }
      } else {
        originalOnClick?.(event);
      }
    };
  }, [hasUnsavedChanges, confirmMessage]);

  return { createGuardedOnClick };
};

export default useUnsavedChangesGuard;

使用方式:

  • 路由拦截:在页面根组件中调用钩子,传入formHasChange
const { createGuardedOnClick } = useUnsavedChangesGuard(formHasChange);
  • 按钮点击:直接用createGuardedOnClick包裹原点击事件
<button onClick={createGuardedOnClick(handleButtonClick)}>提交</button>

2. 用高阶组件替代包装组件(可选)

如果习惯用组件封装的方式,可以基于上面的钩子做一个高阶组件,减少重复代码:

import React from 'react';
import useUnsavedChangesGuard from './useUnsavedChangesGuard';

const withUnsavedChangesGuard = (Component) => {
  return ({ hasUnsavedChanges, onClick, ...props }) => {
    const { createGuardedOnClick } = useUnsavedChangesGuard(hasUnsavedChanges);
    const guardedOnClick = createGuardedOnClick(onClick);
    return <Component onClick={guardedOnClick} {...props} />;
  };
};

export default withUnsavedChangesGuard;

使用方式:

const GuardedButton = withUnsavedChangesGuard(Button);
<GuardedButton hasUnsavedChanges={formHasChange} onClick={handleClick}>按钮</GuardedButton>

3. 全局状态统一管理

如果formHasChange是全局状态,建议用Context或状态管理工具(比如Zustand、Redux)统一维护,避免在多个组件中传递hasChange属性:

示例(Zustand):

import { create } from 'zustand';

const useFormStore = create((set) => ({
  hasUnsavedChanges: false,
  setHasUnsavedChanges: (value) => set({ hasUnsavedChanges: value })
}));

修改钩子直接读取全局状态:

const useUnsavedChangesGuard = (confirmMessage = 'Are you sure you want to discard unsaved changes?') => {
  const hasUnsavedChanges = useFormStore(state => state.hasUnsavedChanges);
  // 其余逻辑不变
};

4. 体验优化:替换原生confirm为自定义弹窗

原生confirm样式无法定制,用户体验一般,可以换成自定义Modal组件:

import { useCallback, useEffect, useState } from 'react';
// 引入你的自定义Modal组件
import CustomConfirmModal from './CustomConfirmModal';

const useUnsavedChangesGuard = (hasUnsavedChanges, confirmMessage = 'Are you sure you want to discard unsaved changes?') => {
  const [showConfirm, setShowConfirm] = useState(false);
  const [pendingAction, setPendingAction] = useState(null);

  // 处理确认/取消逻辑
  const handleConfirm = () => {
    pendingAction?.();
    setShowConfirm(false);
    setPendingAction(null);
  };

  const handleCancel = () => {
    setShowConfirm(false);
    setPendingAction(null);
  };

  // 路由拦截逻辑(略,替换window.confirm为弹窗触发)
  useEffect(() => {
    const handleRouteChange = () => {
      if (hasUnsavedChanges) {
        setPendingAction(() => () => {}); // 路由跳转无需额外操作,取消则抛出错误
        setShowConfirm(true);
        // 这里需要注意:Next.js的routeChangeStart无法直接取消,需要配合抛出错误的逻辑
        throw 'Route Canceled due to unsaved changes';
      }
    };
    // 其余路由监听逻辑不变
  }, [hasUnsavedChanges]);

  // 生成带拦截的点击事件
  const createGuardedOnClick = useCallback((originalOnClick) => {
    return (event) => {
      if (hasUnsavedChanges) {
        event.preventDefault();
        setPendingAction(() => () => originalOnClick?.(event));
        setShowConfirm(true);
      } else {
        originalOnClick?.(event);
      }
    };
  }, [hasUnsavedChanges]);

  // 返回弹窗组件,在页面中渲染
  return {
    createGuardedOnClick,
    ConfirmModal: () => (
      <CustomConfirmModal
        visible={showConfirm}
        message={confirmMessage}
        onConfirm={handleConfirm}
        onCancel={handleCancel}
      />
    )
  };
};

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.06 14:05:20