Как я могу остановить таймер, не используя useState и useEffect?

Вопрос или проблема

В простом компоненте таймера я хочу запускать и останавливать его с помощью кнопок, но интервал не останавливается с помощью простой функции 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>
        </>
    );
}

Объяснение кода

  1. Состояние: Счетчик (seconds) хранит количество прошедших секунд и обновляется через setSeconds.

  2. useRef: Переменная intervalId хранится в рефе, который не сбрасывается при рендерингах. Это ключевой момент для сохранения идентификатора интервала.

  3. startTimer: При каждом нажатии на кнопку "Старт" функция сначала проверяет, не запущен ли таймер (if (intervalId.current) return;). Если таймер не запущен, то используется setInterval для обновления счетчика каждую секунду.

  4. stopTimer: Когда вызывается функция остановки, она очищает интервал с помощью clearInterval и сбрасывает идентификатор интервала.

Заключение

Использование useRef для хранения идентификатора таймера — это простой и эффективный способ решения проблемы с перерасчетом переменных при рендеринге. Такой подход позволяет обеспечить корректную работу таймера, не беспокоясь о повторных запусках и остановках. Вы можете легко адаптировать данный пример под свои нужды, добавляя дополнительные функции, такие как сброс таймера или отображение более сложного формата времени.

Оцените материал
Добавить комментарий

Капча загружается...