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

React中移除useEffect清理函数后keypress事件监听器为何执行两次?

回车键触发两次请求的原因分析

问题场景

编码时遇到一个问题:按回车键发送请求,发现keypress事件监听器执行了两次。后来在组件卸载时添加了事件监听器的移除逻辑,问题就解决了,但不清楚背后的原因。相关代码如下:

const inputRef = useRef(null);
const fileRef = useRef(null);
const [files, setFile] = useState([]);

const sendMessage = useCallback(() => {
    if (inputRef.current.value === '' && files.length === 0) {
        return;
    }

    console.log('f', inputRef.current.value, files);
    if (files.length === 0) {
        AxiosClient.post('/general-msg', {
            orderId,
            recipient,
            content: inputRef.current.value
        });
        inputRef.current.value = '';
    } else {
        const messagePackage = new FormData();
        messagePackage.append('orderId', orderId);
        messagePackage.append('recipient', recipient);
        messagePackage.append('content', inputRef.current.value);

        files.map((file) => {
            return messagePackage.append('file', file);
        });

        AxiosClient.post('/message', messagePackage, {
            headers: {
                'Content-Type': 'multipart/form-data'
            }
        });
        inputRef.current.value = '';
        setFile([]);
    }
}, [files, inputRef, orderId, recipient]);

const registerSendingTrigger = useCallback(
    (e) => {
        console.log('heelo');
        if (e.code === 'Enter') {
            console.log('sending...'); // sending logs twice unless I write below
            sendMessage();
        }
    },
    [sendMessage]
);
useEffect(() => {
    if (inputRef.current) {
        inputRef.current.removeEventListener('keypress', registerSendingTrigger);

        inputRef.current.addEventListener('keypress', registerSendingTrigger);
    }

    return () => inputRef.current.removeEventListener('keypress', registerSendingTrigger); // this part!!!!!!!!

    // why does send request gets twice when I delete return state??
}, [inputRef, registerSendingTrigger]);
useEffect(() => {
    if (fileRef) {
        fileRef.current.addEventListener('change', (e) => {
            if (!e.target.files[0]) {
                return;
            }
            setFile((current) => {
                return [...current, e.target.files[0]];
            });
        });
    }
}, []);

原因解析

1. 重复绑定事件监听器的根源

你的useEffect依赖了registerSendingTrigger,而registerSendingTrigger又依赖sendMessage。当files状态更新时(比如添加文件后),sendMessage会因为依赖项变化重新创建,进而导致registerSendingTrigger也生成新的函数引用。

此时如果没有清理函数:

  • 每次useEffect重新执行时,虽然你调用了removeEventListener,但传入的是新的registerSendingTrigger引用,无法移除之前绑定的旧版本监听器(因为removeEventListener要求必须和addEventListener使用完全相同的函数引用才能生效)。
  • 旧的监听器依然绑定在输入框元素上,新的监听器又被添加进去,最终输入框上会存在多个keypress监听器实例。

2. 清理函数的作用

useEffect的返回函数是清理函数,它会在两个时机执行:

  • 组件卸载时;
  • 当前useEffect因为依赖项变化重新执行前。

添加清理函数后,每次重新绑定新的监听器前,都会用当前的registerSendingTrigger引用移除旧的监听器(组件卸载时也会彻底移除),确保输入框上始终只有一个有效的keypress监听器,自然不会出现触发两次的情况。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.22 17:18:11