Использование переменных состояния внутри интервального обратного вызова

Я разрабатываю небольшую многопользовательскую веб-игру, в которой игрок отправляет ответы на некоторые вопросы в течение определенного времени.

У меня есть это состояние «ответ», которое используется для ввода и совпадает с содержимым поля ввода:

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])

Не могу понять, это разные компы или файлы?

AL Faiz Ahmed 29.02.2024 11:14

Можете ли вы поделиться своим useEffect?

Nick Parsons 29.02.2024 11:17

@ALFaizAhmed это всего лишь один файл, один и тот же компонент. Я показал вам лишь несколько минимальных фрагментов, потому что на самом деле это более сложно.

Crih.exe 29.02.2024 11:23

И sharedEventSource источник, который является частью вашей зависимости useEffect, не меняется, верно? Это одно и то же для всех рендеров?

Nick Parsons 29.02.2024 11:24

@NickParsons да, это то же самое, это общий источник событий для получения событий sse с сервера, полученных из useContext

Crih.exe 29.02.2024 11:25

@Crih.exe Определен ли sendAnswer в том же компоненте, что и ваш useEffect? или вы передаете его как реквизит компоненту, который его использует?

Nick Parsons 29.02.2024 11:34

Давайте продолжим обсуждение в чате.

Crih.exe 29.02.2024 11:35
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Улучшение производительности загрузки с помощью Google Tag Manager и атрибута Defer
Улучшение производительности загрузки с помощью Google Tag Manager и атрибута Defer
В настоящее время производительность загрузки веб-сайта имеет решающее значение не только для удобства пользователей, но и для ранжирования в...
Безумие обратных вызовов в javascript [JS]
Безумие обратных вызовов в javascript [JS]
Здравствуйте! Юный падаван 🚀. Присоединяйся ко мне, чтобы разобраться в одной из самых запутанных концепций, когда вы начинаете изучать мир...
Система управления парковками с использованием HTML, CSS и JavaScript
Система управления парковками с использованием HTML, CSS и JavaScript
Веб-сайт по управлению парковками был создан с использованием HTML, CSS и JavaScript. Это простой сайт, ничего вычурного. Основная цель -...
JavaScript Вопросы с множественным выбором и ответы
JavaScript Вопросы с множественным выбором и ответы
Если вы ищете платформу, которая предоставляет вам бесплатный тест JavaScript MCQ (Multiple Choice Questions With Answers) для оценки ваших знаний,...
0
7
55
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

На данный момент ваша функция обратного вызова 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()...} каждую секунду?

Crih.exe 29.02.2024 21:33

Другие вопросы по теме