这是一个 socket.io 聊天应用程序,我试图在其中添加键盘快捷键。我正在侦听“keydown”事件,并在按下特定组合键时调用按钮单击侦听器函数(也受到限制)。
问题是,当按钮的单击事件调用单击侦听器时,它会按预期工作,但是当从 keydown 侦听器触发侦听器时,它没有状态的更新值,因为它不执行任何操作即可返回。
这些是我的状态变量:
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())
/* When I click the button the listener logs:
"userID 4W_o0nfOpLPknesfAA room 4W_o0nfOpLPknesfAA#NJsYxkxSVVfuQBzrAA message hello there"
But when I use the keyboard shortcut the listener logs:
"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) // can run only after 0.4s after the last call
然后有一个Keydown监听器:
function keyShortcuts(e: KeyboardEvent) {
if (e.ctrlKey && e.key === 'Enter') {
handleSubmitClick();
}
}
监听器被添加到 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)
}
}, []);
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 button component */}
您遇到的问题是由于关闭造成的。在初始渲染时,当您调用 useEffect 挂钩来连接套接字并添加窗口事件侦听器时 - 作为窗口 keydown 事件侦听器附加的函数正在使用该函数内部使用的所有变量的初始值,这些变量不会通过未来的重新渲染以任何方式更新。
由于每次重新渲染,直接单击按钮不会发生这种情况 - 设置新的状态变量并重新创建函数
handleSubmitClick
,它确实会看到这种情况下的所有最近更新并按预期运行。
一个建议是利用
useRef
挂钩来保存 keydown 事件侦听器和 handleSubmitClick
所需的所有内容的最新值。我发现了一个小实用程序钩子,它充当 useState
钩子,但也返回一个 ref
变量:
function useStateWithRef<T>(
initialValue: T
): [T, Dispatch<SetStateAction<T>>, React.MutableRefObject<T>] {
const [state, setState] = useState<T>(initialValue);
const ref = useRef<T>(state);
const setStateAndRef: Dispatch<SetStateAction<T>> = (newState) => {
if (typeof newState === "function") {
setState((prevState) => {
const computedState = (newState as (prevState: T) => T)(prevState);
ref.current = computedState;
return computedState;
});
} else {
setState(newState);
ref.current = newState;
}
};
return [state, setStateAndRef, ref];
}
用途:
const [userID, setUserID, userIDRef] = useStateWithRef<string>("");
const [messages, setMessages, messagesRef] = useStateWithRef<TMessage[]>([]);
const [messageInput, setMessageInput, messageInputRef] =
useStateWithRef<string>("");
const [room, setRoom, roomRef] = useStateWithRef<string | null>(null);
const handleSubmitClick = throttle(() => {
console.log(
"userID",
userIDRef.current,
"room",
room.current,
"message",
messageInput.current.trim()
);
// ...etc
}, 400);
旁注 - 您没有在 useEffect 中删除窗口 keydown 事件侦听器,请修复该问题。
另一种方法 - 用 useCallback 钩子包装你的函数
keyShortcuts
和 handleSubmitClick
并添加一个 useEffect ,它将在状态更改时附加/分离 keyShortcuts
:
const lastSocketActionDateMsRef = useRef(0);
const handleSubmitClick = useCallback(() => {
// can run only after 0.4s after the last call
if (lastSocketActionDateMsRef.current + 400 < Date.now()) return;
lastSocketActionDateMsRef.current = Date.now();
console.log("userID", userID, "room", room, "message", messageInput.trim());
if (
!socket.connected ||
userID === "" ||
room === null ||
messageInput.trim() === ""
)
return;
const data = {
userID: socket.id!,
message: messageInput.trim(),
room: room,
};
socket.emit("CHAT_SEND", data);
}, [userID, room, messageInput, socket]);
const keyShortcuts = useCallback(
(e: KeyboardEvent) => {
if (e.ctrlKey && e.key === "Enter") {
handleSubmitClick();
}
},
[handleSubmitClick]
);
useEffect(() => {
window.addEventListener("keydown", keyShortcuts);
return () => {
window.removeEventListener("keydown", keyShortcuts);
};
}, [keyShortcuts]);
假设handleSubmitClick 具有有关套接字状态、用户ID、输入值的所有检查 - 如果未设置某些内容,则将是空操作。当相关状态更改时,Keydown 事件侦听器将在每次重新渲染时设置和取消设置。
但是,在这种情况下,throttle 功能会出现一些问题 -throttle 和 useCallback 不能很好地配合,这里有一些细节和解决方法,我建议删除throttle 并添加
const lastCalledRef = useRef(Date.now)
和只需使用此变量检查上次执行时间。