Видеоуроки, интерактивный редактор и сохранение прогресса — бесплатно, сразу после входа.
ВойтиСоздать аккаунт — бесплатноЗакончили урок?
Войдите, чтобы отмечать прогресс
В этом решении мы создаём таймер обратного отсчёта с использованием хука useEffect для управления интервалами.
Мы храним четыре состояния: inputMinutes для количества минут, которое устанавливает пользователь, timeLeft для оставшегося времени в секундах (60 секунд = 1 минута), isRunning для отслеживания, запущен ли таймер, и isPaused для состояния паузы. Время храним в секундах, потому что интервал работает каждую секунду.
useEffect выполняется после каждого рендера, но только если изменились зависимости в массиве [isRunning, isPaused, timeLeft]. Первые две проверки - это ранние выходы: если таймер не запущен или на паузе, эффект ничего не делает. Если время вышло, останавливаем таймер. Функция setInterval создаёт интервал, который выполняется каждую секунду (1000 миллисекунд).
Это cleanup функция - она вызывается перед следующим выполнением эффекта или при размонтировании компонента. clearInterval(timer) останавливает интервал. Без cleanup интервалы накапливались бы - каждое изменение состояния создавало бы новый интервал, а старые продолжали работать. Это вызвало бы утечку памяти и неправильное поведение.
Мы используем функциональную форму setState с параметром prev (предыдущее значение). Это важно для работы с интервалами - callback внутри setInterval создаётся один раз и "замыкает" значения переменных. Если бы мы писали setTimeLeft(timeLeft - 1), значение timeLeft всегда было бы тем, каким оно было при создании интервала. Функциональная форма гарантирует работу с актуальным значением.
Когда осталась 1 секунда, на следующем тике мы устанавливаем время в 0 и останавливаем таймер. Проверка <= 1 (а не === 0) - это защита от возможных проблем с синхронизацией. Мы гарантируем, что таймер остановится, даже если что-то пойдёт не так.
При запуске мы проверяем, не запущен ли таймер уже. Если нет, устанавливаем время из inputMinutes, переводя минуты в секунды умножением на 60. Затем помечаем таймер как запущенный и снимаем паузу. Эта функция используется и для первого старта, и для возобновления после остановки.
Пауза просто устанавливает флаг isPaused в true. В useEffect есть проверка этого флага - если он true, интервал не создаётся. Возобновление сбрасывает флаг обратно в false, и таймер продолжает работу с того же места.
Полная остановка сбрасывает все флаги и обнуляет время. Таймер нельзя будет просто возобновить - нужно начать заново. Это отличается от паузы, которая сохраняет оставшееся время.
Сброс возвращает таймер к исходному состоянию с временем из inputMinutes. Пользователь может нажать "Сброс" после завершения или остановки, чтобы вернуться к начальному значению без изменения настроек.
Math.floor(seconds / 60) даёт количество полных минут - например, 125 секунд это 2 минуты. Оператор % (остаток от деления) даёт секунды - 125 % 60 = 5 секунд. Метод padStart(2, '0') добавляет ведущий ноль, если число однозначное: '5' становится '05'. Это даёт формат как на цифровых часах: 02:05.
Input с type="number" возвращает строку. parseInt() преобразует её в число. Оператор || работает как значение по умолчанию - если parseInt вернёт NaN (при пустом поле), используется 1. Это предотвращает ошибки при некорректном вводе.
Когда пользователь меняет минуты в input, мы обновляем и inputMinutes, и timeLeft одновременно. Это гарантирует, что отображаемое время всегда соответствует значению в поле ввода. Без обновления timeLeft на дисплее продолжало бы показываться старое время.
Поле ввода показывается только когда таймер не запущен И время равно начальному значению. Если таймер был запущен и остановлен (время не равно начальному), поле скрыто. Это предотвращает изменение настроек во время работы таймера или после его использования.
Каждая кнопка показывается только в подходящей ситуации. "Старт" - когда таймер остановлен и есть время. "Пауза" - когда работает и не на паузе. "Продолжить" - когда на паузе. Это создаёт интуитивный интерфейс - пользователь видит только те действия, которые имеют смысл в текущий момент.
Кнопка "Сброс" появляется, когда время изменилось (таймер был использован) И таймер не запущен. Условие timeLeft !== inputMinutes * 60 проверяет, что время отличается от начального, или timeLeft === 0 - время закончилось. Когда таймер в начальном состоянии, сброс не нужен.