Почему setTimeout в React видит старое значение state
Введите в поле «Первый», нажмите кнопку, а затем, пока таймер не сработал, замените текст на «Второй»:
import { useState } from "react";
export default function Demo() {
const [text, setText] = useState("");
function handleClick() {
setTimeout(() => {
console.log(text);
}, 3000);
}
return (
<>
<input
value={text}
onChange={(e) => setText(e.target.value)}
/>
<button onClick={handleClick}>
Вывести через 3 секунды
</button>
</>
);
}
В консоли появится «Первый», хотя в поле уже «Второй».
React здесь успел обновить состояние. Но при каждом рендере функция компонента вызывается заново — со своим значением text и новыми обработчиками. Колбэк уже запущенного таймера сохраняет через замыкание доступ к text из предыдущего рендера.
Новый рендер обновит поле на экране, но не заменит колбэк внутри этого таймера.
А какое значение нам нужно?
Если пользователь зафиксировал текст для отложенной отправки, значение на момент клика может быть правильным. Последующие правки не обязательно должны менять уже запланированное сообщение.
Если же нужно прочитать последнее введённое значение на момент срабатывания таймера, один из вариантов — useRef.
Добавим useRef в импорт:
import { useRef, useState } from "react";
Внутри компонента создадим ref:
const latestText = useRef("");
Обновим обработчик поля:
onChange={(e) => {
const value = e.target.value;
latestText.current = value;
setText(value);
}}
А внутри колбэка таймера заменим чтение text:
console.log(latestText.current);
Теперь колбэк обращается к объекту ref, который сохраняется между рендерами, и читает его текущее свойство current.
Сам ref за состоянием не следит: в этом примере мы обновляем его при вводе. text по-прежнему нужен для отображения значения в поле — изменение ref.current не вызывает рендер.
Получается, «старое значение» не всегда означает ошибку. Сначала стоит определить, что должно использовать отложенное действие: текст на момент клика или текст на момент выполнения.
Поделитесь, должны ли последующие правки в вашей форме «Отправить позже» менять уже запланированное сообщение?