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按钮组件 */}
原因分析
- 闭包陷阱:初始渲染时创建的
handleSubmitClick函数捕获了当时的state值(空字符串、null等),后续state更新后,这个旧函数依然引用旧的state。 - 事件监听器引用过时:键盘事件监听器
keyShortcuts是在socket连接时添加到window上的,它引用的是初始版本的handleSubmitClick,所以快捷键触发的始终是旧函数,拿不到最新state。 - 按钮点击正常的原因:按钮的
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
相关产品推荐
相关产品推荐

