Это приложение для чата Socket.io, в котором я пытаюсь добавить сочетание клавиш. Я слушаю событие «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.info('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)
}
}, []);
Наконец, в пользовательском интерфейсе есть кнопка и текстовая область:
<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 */}
🤔 А знаете ли вы, что...
С React можно работать с данными через HTTP-запросы, используя библиотеки, такие как Axios или Fetch API.
Проблема, с которой вы столкнулись, связана с замыканиями. При первоначальном рендеринге, когда вы вызываете перехватчик useEffect для подключения сокета и добавления прослушивателя событий окна, функция, которая подключена как прослушиватель событий нажатия клавиши окна, использует начальные значения всех переменных, которые используются внутри этой функции. , эти переменные никак не обновляются при будущих повторных отрисовках.
И этого не происходит при прямом нажатии кнопки из-за каждого повторного рендеринга — устанавливаются новые переменные состояния и функция handleSubmitClick
воссоздается, в этом случае она видит все последние обновления и действует так, как ожидалось.
Одно из предложений — использовать крючок useRef
для хранения последних значений всего, что вам нужно для прослушивателя событий нажатия клавиш и 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.info(
"userID",
userIDRef.current,
"room",
room.current,
"message",
messageInput.current.trim()
);
// ...etc
}, 400);
Примечание: вы не удаляете прослушиватель событий нажатия клавиши окна в своем useEffect, исправьте это, пожалуйста.
Другой подход — оберните ваши функции keyShortcuts
и handleSubmitClick
хуками useCallback и добавьте 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.info("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 имеет все проверки состояния сокета, userId, входного значения - если что-то не установлено, это будет ошибка. Прослушиватель событий Keydown будет устанавливаться и отключаться при каждом повторном рендеринге при изменении связанных состояний. Только не забудьте удалить window.addEventListener из useEffect, который обрабатывает инициализацию сокета.
Но в этом случае возникнут некоторые проблемы с функцией дроссельной заслонки - дроссельная заслонка и useCallback не работают друг с другом, вот некоторые подробности и обходные пути, и я бы предложил удалить дроссельную заслонку, добавить const lastSocketActionDateMsRef = useRef(0)
и просто проверить время последнего выполнения с использованием этой переменной.