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
相关产品推荐
相关产品推荐

