Вопрос или проблема
В простом компоненте таймера я хочу запускать и останавливать его с помощью кнопок, но интервал не останавливается с помощью простой функции clearInterval. Неужели я что-то упускаю?
import React, { useState } from 'react'
export default function Timer3() {
const [seconds, setseconds] = useState(0)
let intervalId;
const startTimer = () => {
intervalId = setInterval(() => {
setseconds((pre) => pre + 1)
}, 1000)
}
const stopTimer = () => {
clearInterval(intervalId)
}
return (
<>
{seconds}
<button onClick={startTimer}>начать</button>
<button onClick={stopTimer}>остановить</button>
</>
)
}
Я знаю, что в приведенном выше коде есть ошибка: если я нажму «Начать» дважды, он будет считать дважды. Я собираюсь исправить это после того, как выясню, как его остановить. В сети есть также решение использовать useEffect для запуска таймера, но я не ищу код — просто простой ответ на вопрос, почему это не работает.
Проблема в том, что каждый раз, когда состояние обновляется, компонент перерисовывается, и переменная intervalId
повторно объявляется, и любое предыдущее значение теряется, поэтому, когда кнопка остановки нажата, у нее нет ссылки на идентификатор работающего таймера.
Вы можете исправить это, используя ссылку React для хранения значения идентификатора интервала. Ссылки React сохраняются от одного цикла рендеринга к другому.
function Timer3() {
const [seconds, setseconds] = React.useState(0);
const intervalId = React.useRef();
React.useEffect(() => {
// Очистите любые работающие таймеры, когда компонент размонтируется
return () => {
clearInterval(intervalId.current);
}
}, []);
const startTimer = () => {
// Очистите любые работающие таймеры перед перезапуском
clearInterval(intervalId.current);
intervalId.current = setInterval(() => {
setseconds((pre) => pre + 1);
}, 1000);
};
const stopTimer = () => {
clearInterval(intervalId.current);
};
return (
<>
{seconds}
<button onClick={startTimer}>начать</button>
<button onClick={stopTimer}>остановить</button>
</>
);
}
const rootElement = document.getElementById("root");
const root = ReactDOM.createRoot(rootElement);
root.render(
<React.StrictMode>
<Timer3 />
</React.StrictMode>
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.3.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.3.1/umd/react-dom.production.min.js"></script>
<div id="root" />
intervalId сбрасывается каждый раз, когда компонент перерисовывается, так как это обычная переменная.
Вы можете использовать useRef вместо useState и useEffect, useRef сохраняет значение и не перерисовывается при изменении.
const intervalId = useRef(null);
В startTimer
присвойте интервал intervalId.current
вместо просто intervalId
.
В stopTimer
вызовите clearInterval(intervalId.current)
.
Вы можете хранить intervalId в ссылке useRef вместо того, чтобы повторно объявлять его каждый раз:
import React, { useState, useRef } from 'react';
export default function Timer3() {
const [seconds, setSeconds] = useState(0);
const intervalId = useRef(null);
const startTimer = () => {
if (intervalId.current) return;
intervalId.current = setInterval(() => {
setSeconds((prev) => prev + 1);
}, 1000);
};
const stopTimer = () => {
clearInterval(intervalId.current);
intervalId.current = null;
};
return (
<>
{seconds}
<button onClick={startTimer}>Начать</button>
<button onClick={stopTimer}>Остановить</button>
</>
);
}
.
Ответ или решение
Чтобы остановить таймер в компоненте React без использования useState
и useEffect
, можно воспользоваться хуком useRef
, который позволяет хранить значение между рендерингами без его перезагрузки. Это важно, поскольку в вашем текущем коде переменная intervalId
пересоздается при каждом рендере, что приводит к потере предыдущего идентификатора интервала и в итоге не позволяет корректно остановить таймер.
Реализация с использованием useRef
Вот как можно переписать ваш компонент, используя useRef
для хранения идентификатора интервала:
import React, { useState, useRef } from 'react';
export default function Timer3() {
const [seconds, setSeconds] = useState(0); // Состояние для хранения прошедших секунд
const intervalId = useRef(null); // Используем useRef для хранения идентификатора интервала
const startTimer = () => {
if (intervalId.current) return; // Проверяем, запущен ли таймер
intervalId.current = setInterval(() => {
setSeconds(prev => prev + 1); // Увеличиваем счетчик на 1 каждую секунду
}, 1000);
};
const stopTimer = () => {
clearInterval(intervalId.current); // Останавливаем интервал
intervalId.current = null; // Сбрасываем идентификатор
};
return (
<>
{seconds} секунд
<button onClick={startTimer}>Старт</button>
<button onClick={stopTimer}>Стоп</button>
</>
);
}
Объяснение кода
-
Состояние: Счетчик (
seconds
) хранит количество прошедших секунд и обновляется черезsetSeconds
. -
useRef: Переменная
intervalId
хранится в рефе, который не сбрасывается при рендерингах. Это ключевой момент для сохранения идентификатора интервала. -
startTimer: При каждом нажатии на кнопку "Старт" функция сначала проверяет, не запущен ли таймер (
if (intervalId.current) return;
). Если таймер не запущен, то используетсяsetInterval
для обновления счетчика каждую секунду. -
stopTimer: Когда вызывается функция остановки, она очищает интервал с помощью
clearInterval
и сбрасывает идентификатор интервала.
Заключение
Использование useRef
для хранения идентификатора таймера — это простой и эффективный способ решения проблемы с перерасчетом переменных при рендеринге. Такой подход позволяет обеспечить корректную работу таймера, не беспокоясь о повторных запусках и остановках. Вы можете легко адаптировать данный пример под свои нужды, добавляя дополнительные функции, такие как сброс таймера или отображение более сложного формата времени.