Я разрабатываю небольшую многопользовательскую веб-игру, в которой игрок отправляет ответы на некоторые вопросы в течение определенного времени.
У меня есть это состояние «ответ», которое используется для ввода и совпадает с содержимым поля ввода:
const [answer, setAnswer] = useState("")
return (
<>
{/*...*/}
<input type = "text"
value = {answer}
onInput = {e => setAnswer(e.target.value)}/>
{/*...*/}
</>
)
И кнопка для отправки ответа до звонка таймера:
<div onClick = {() => {sendAnswer(false)}}>
Функция sendAnswer используется для отправки на сервер всего, что игрок написал в этом поле ввода, вместе с некоторыми другими состояниями, используя данные внутри состояния «ответ». Эта функция объявляется внутри функции компонента следующим образом:
function GameRound() {
// ...
const sendAnswer = async (autoSent) => {
// ...
fetch("...endpoint...", {
// ...
body: JSON.stringify({
"answer": answer,
"ready": !autoSent,
"timeRemaining": timerTime
})
});
// ...
}
// ...
}
У меня также есть интервал, уменьшающий переменную таймера, который инициализируется внутри useEffect в [] и вызывает «sendAnswer», когда он достигает 0.
Когда игрок запускает «sendAnswer», нажав на кнопку, все в порядке, и тело запроса к серверу соответствует фактическим данным используемой переменной состояний. Но когда «sendAnswer» срабатывает по интервалу, данные, передаваемые в тело запроса, представляют собой всего лишь начальные значения, установленные для состояний при их создании с помощью useState.
Я думаю, это из-за обратного вызова sendAnswer вне ловушки, но я не могу найти другого способа!
Обновлено: мой useEffect
const [timerTime, setTimerTime] = useState(30);
useEffect(() => {
const updateTimer = () => {
setTimerTime((oldTimerTime) => {
if (oldTimerTime === 1) {
if (canSendRef.current)
sendAnswer(true)
}
return oldTimerTime-1
});
};
timerInterval = setInterval(updateTimer, 1000);
}, [sharedEventSource])
Можете ли вы поделиться своим useEffect?
@ALFaizAhmed это всего лишь один файл, один и тот же компонент. Я показал вам лишь несколько минимальных фрагментов, потому что на самом деле это более сложно.
И sharedEventSource источник, который является частью вашей зависимости useEffect, не меняется, верно? Это одно и то же для всех рендеров?
@NickParsons да, это то же самое, это общий источник событий для получения событий sse с сервера, полученных из useContext
@Crih.exe Определен ли sendAnswer в том же компоненте, что и ваш useEffect? или вы передаете его как реквизит компоненту, который его использует?
Давайте продолжим обсуждение в чате.



![Безумие обратных вызовов в javascript [JS]](https://i.imgur.com/WsjO6zJb.png)


На данный момент ваша функция обратного вызова useEffect() создается при первоначальном монтировании вашего компонента GameRound и устанавливает интервал. Функция обратного вызова useEffect() знает о переменных в окружающей ее области, которая включает в себя такие вещи, как состояние и функции (т. е.: sendAnswer). Однако, поскольку ваша зависимость useEffect() никогда не меняется при повторных рендерингах (судя по вашим комментариям), функция обратного вызова useEffect вызывается только один раз при монтировании, и поэтому она знает только о состоянии и функциях, созданных для этого первоначального рендеринга, где значения вашего состояния все сохраняют свои первоначальные значения. Таким образом, весь код, вложенный в обратный вызов, «видит» только значения переменных из первоначального рендеринга/вызова вашего функционального компонента (это часто называют устаревшим замыканием).
Типичный способ обойти это — передать sendAnswer как зависимость вашему useEffect(), однако это вызовет всевозможные проблемы с остановкой и запуском таймера при изменении состояния answer. К сожалению, лучший способ обойти это — еще официально не выпущен в React. Однако на данный момент мы можем сами создать хук, имитирующий эту еще не реализованную функцию, создав собственную псевдоверсию useEffectEvent хука:
// Rough example of how `useEffectEvent` could be implemented.
// Taken from: https://github.com/reactjs/rfcs/blob/useevent/text/0000-useevent.md#internal-implementation
import { useRef, useLayoutEffect, useCallback } from "react";
function useEffectEvent(handler) {
const handlerRef = useRef(null);
useLayoutEffect(() => {
handlerRef.current = handler;
});
return useCallback((...args) => {
const fn = handlerRef.current;
return fn(...args);
}, []);
}
Теперь, чтобы использовать его, я бы сначала посоветовал вам избегать добавления побочных эффектов в вашу функцию установки состояния. Как вы упомянули в своем комментарии ниже, ваш интервал добавляется обработчиком событий.
Вы можете немного реорганизовать свой обратный вызов useEffect примерно так:
const decTimer = useEffectEvent(() => {
if (timerTime === 1 && canSendRef.current);
sendAnswer(true);
setTimerTime(oldTimerTime => oldTimerTime-1); // no side-effects anymore (your state setter function is now a pure function, as React recommends).
});
useEffect(() => {
let timerInterval;
sharedEventSource.addEventListener("game", (event) => {
timerInterval = setInterval(() => decTimer(), 1000);
});
return () => {
sharedEventSource.removeEventListener("game");
clearInterval(timerInterval);
};
}, [sharedEventSource]); // no need to specify `decTimer` as a dependency as it is stable across renders (same function reference for every rerender)
При такой настройке canSendRef.current также может оставаться в состоянии (а не в качестве ссылки), так как теперь decTimer имеет доступ к самому актуальному состоянию.
большое спасибо! Кстати, у меня был бы вопрос в примечании. Предоставленный мной код упрощен, поскольку я пропустил, что в моем useEffect я использую общийEventSource для добавления прослушивателя событий в «игровых» событиях, которые представляют собой событие, поступающее с сервера, сообщающее клиенту начать раунд и, следовательно, интервал . Итак, timerInterval устанавливается внутри этого прослушивателя событий. Будет ли добавление timerTime к зависимостям вызывать проблемы со слушателем, поскольку он будет срабатывать sharedEventSource.addEventListener("game", (event) => {...setInterval()...} каждую секунду?
Не могу понять, это разные компы или файлы?