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

React状态已更新,但键盘快捷键触发的事件监听器无法获取最新状态

问题:Ctrl+Enter快捷键触发节流函数无法获取最新React State

问题描述

基于socket.io开发聊天应用时,添加了Ctrl+Enter发送消息的快捷键功能:监听window的keydown事件,按下组合键时调用按钮的点击处理函数(已做节流处理)。但出现异常:

  • 点击按钮触发handleSubmitClick时,能正常获取最新state值,消息发送正常;
  • 用Ctrl+Enter快捷键触发时,函数拿到的是初始state值(userID为空、room为null、messageInput为空),直接返回不执行发送逻辑。

相关代码

状态变量

const [userID, setUserID] = useState<string>('');
const [messages, setMessages] = useState<TMessage[]>([]);
const [messageInput, setMessageInput] = useState<string>('');
const [room, setRoom] = useState<string | null>(null);

点击处理函数(带节流)

const handleSubmitClick = throttle(() => {
    console.log('userID', userID, 'room', room, 'message', messageInput.trim())
    /* 按钮点击日志:
       "userID 4W_o0nfOpLPknesfAA room 4W_o0nfOpLPknesfAA#NJsYxkxSVVfuQBzrAA message hello there"
       快捷键触发日志:
       "userID <empty string> room null message <empty string>"
    */

    if (!socket.connected || userID === '' || room === null || messageInput.trim() === '')
      return;

    const data: TMessage = { userID: socket.id!, message: messageInput.trim(), room: room };
    socket.emit(SocketEvents.CHAT_SEND, data);
  }, 400) // 距上次调用0.4秒后才可再次执行

键盘事件监听

function keyShortcuts(e: KeyboardEvent) {
    if (e.ctrlKey && e.key === 'Enter') {
      handleSubmitClick();
    }
}

useEffect(() => {
  if (!socket.connected) {
    socket.connect();
  }
  function onConnect() {
    setUserID(socket.id!);
    window.addEventListener('keydown', keyShortcuts);
  }
  socket.on(SocketEvents.CONNECT, onConnect)

  return () => {
    socket.disconnect()
      .off(SocketEvents.CONNECT, onConnect)
  }
}, []);

UI组件

<textarea value={messageInput}
  onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => setMessageInput(e.target.value)}
  disabled={disabled}
/>
<Primary label="Send" subtitle="(ctrl + Enter)" 
  handleClick={handleSubmitClick} disabled={disabled}
/> {/* React按钮组件 */}

原因分析

  1. 闭包陷阱:初始渲染时创建的handleSubmitClick函数捕获了当时的state值(空字符串、null等),后续state更新后,这个旧函数依然引用旧的state。
  2. 事件监听器引用过时:键盘事件监听器keyShortcuts是在socket连接时添加到window上的,它引用的是初始版本的handleSubmitClick,所以快捷键触发的始终是旧函数,拿不到最新state。
  3. 按钮点击正常的原因:按钮的handleClick属性在每次组件重渲染时都会接收最新创建的handleSubmitClick函数(每次渲染都会重新执行throttle(...)创建新函数),所以能获取最新state,但这种写法会导致节流失效(每次点击都是新的节流函数,400ms限制不起作用)。

解决方法

方案:用useRef保存最新状态+useCallback稳定函数引用

通过ref存储最新的state值,避免闭包引用过时的state,同时用useCallback确保节流函数的引用稳定:

// 1. 创建ref存储最新状态
const stateRef = useRef({
  userID: '',
  room: null,
  messageInput: '',
  socketConnected: false
});

// 2. 同步state到ref,每次state更新时执行
useEffect(() => {
  stateRef.current = {
    userID,
    room,
    messageInput,
    socketConnected: socket.connected
  };
}, [userID, room, messageInput, socket.connected]);

// 3. 节流函数从ref读取最新状态,用useCallback确保函数引用稳定
const handleSubmitClick = useCallback(throttle(() => {
  const { userID, room, messageInput, socketConnected } = stateRef.current;
  
  if (!socketConnected || userID === '' || room === null || messageInput.trim() === '')
    return;

  const data: TMessage = { userID: socket.id!, message: messageInput.trim(), room: room };
  socket.emit(SocketEvents.CHAT_SEND, data);
}, 400), []);

// 4. 确保键盘事件监听器引用最新的handleSubmitClick
const keyShortcuts = useCallback((e: KeyboardEvent) => {
  if (e.ctrlKey && e.key === 'Enter') {
    handleSubmitClick();
  }
}, [handleSubmitClick]);

// 5. 更新useEffect的依赖,确保监听器能更新并清理旧监听器
useEffect(() => {
  if (!socket.connected) {
    socket.connect();
  }
  function onConnect() {
    setUserID(socket.id!);
    window.addEventListener('keydown', keyShortcuts);
  }
  socket.on(SocketEvents.CONNECT, onConnect);

  return () => {
    socket.disconnect().off(SocketEvents.CONNECT, onConnect);
    window.removeEventListener('keydown', keyShortcuts);
  };
}, [keyShortcuts]);

额外优化

原来的handleSubmitClick每次渲染都会重新创建节流函数,导致节流效果失效。用useCallback包裹后,函数引用稳定,节流才能正常工作。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.21 10:34:58