Не секрет, что такие функции как setInterval
, setTimeout
очень хотят быть уничтоженными
при размонтировании компонента во избежание утечек памяти. И если с компонентами-классами всё понятно,
то так ли очевидны способы взаимодействия с таймерами в функциональных компонентах?
Компонент-класс
В классах заботу о таймерах на себя берёт метод жизненного цикла componentWillUnmount
.
Объявляем таймер и оставляем его на совесть методу.
import { Component } from 'react';
class Timer extends Component {
timer;
// таймер срабатывает по клику
startTimer = () => {
this.timer = setTimeout(() => {
console.log('start this.timer');
}, 500);
}
componentWillUnmount() {
clearTimeout(this.timer); // очистка таймера
}
render() {
return <button onClick={this.startTimer}>Start timer</button>;
}
}
Функциональный компонент
В функциональных компонентах нет возможности задать таймер как константу — это не сработает из-за
особенностей рендера. Чтобы таймер запускался при каком-то пользовательском действии как в предыдущем примере,
нужно привязать его через useRef
.
import { useRef, useEffect } from 'react';
export const Timer = () => {
const timerRef = useRef(null);
// таймер срабатывает по клику
const startTimer = () => {
timerRef.current = setTimeout(() => {
console.log('start current timeout');
}, 500);
}
useEffect(() => {
return () => clearTimeout(timerRef.current); // очистка таймера
}, []);
return <button onClick={startTimer}>Start timer</button>;
}
Если пользовательских действий не нужно, на помощь придут пользовательские хуки: с ними не придётся дублировать код таймера каждый раз.
Хук useTimeout
Аналог таймера, данного выше. Только запускаться он будет сразу после монтирования того компонента, к которому подключен.
import { useEffect, useRef } from 'react';
export function useTimeout(callback, delay) {
// Запомнить переданную функцию обратного вызова
const savedCallback = useRef(callback);
// переопределять callback при необходимости
useEffect(() => {
savedCallback.current = callback;
}, [callback]);
// Запустить таймер
useEffect(() => {
// или не запустить когда не задано время задержки
if (delay === null) return;
const timerId = setTimeout(() => savedCallback.current(), delay);
return () => clearTimeout(timerId);
}, [delay])
}
Как использовать в компоненте:
// передать в качестве handleCallback функцию,
// которая должна быть вызвана по таймауту
useTimeout(handleCallback, 500);
Хук useInterval
Теперь что-нибудь поинтереснее с интервалом. На многих сайтах есть такой кейс: при вводе логина получать одноразовый пароль по SMS и, если в течение некоторого времени SMS не приходит, отправлять запрос заново. Тут и может пригодиться наш хук.
import { useState, useEffect } from 'react';
export function useInterval(time, delay) {
// запомнить переданное время в секундах
const [timeLeft, setTimeLeft] = useState(time);
useEffect(() => {
// не запускать когда не задано время задержки
if (delay === null) return;
// уменьшать время на единицу
const tick = () => {
setTimeLeft(timeLeft - 1);
};
// старт
const timerId = setInterval(tick, delay);
// остановить если время истекло
if (timeLeft <= 0) clearInterval(id);
// очистить интервал
return () => clearInterval(timerId);
}, [delay, timeLeft]);
// передать управление интервалом вовне
return [timeLeft, setTimeLeft];
}
Как использовать в компоненте:
// импорт хука
import { useInterval } from '../hooks/useInterval';
const ONE_SECOND = 1000;
const ONE_MINUTE = 60;
const formatTime = time => time > 9 ? time : `0${time}`;
export const Timeout = () => {
// получить данные из хука
// задать время 2 мнуты, остаток менять с интервалом в секунду
const [timeLeft, setTimeLeft] = useInterval(ONE_MINUTE * 2, ONE_SECOND);
// используем timeLeft для привычного отображения времени
const minutes = formatTime(Math.floor(timeLeft / ONE_MINUTE));
const seconds = formatTime(timeLeft - minutes * ONE_MINUTE);
const handleStartTimer = () => {
setTimeLeft(ONE_MINUTE);
};
return (
<div>
{
timeLeft > 0
? <p>До повторной отправки осталось: {minutes}:{seconds}</p>
: <button onClick={handleStartTimer}>Отправить SMS</button>
}
</div>
)
};
Пока время не истекло, будет выводиться счётчик с таймером. После — кнопка с возможностью сбросить счётчик и запустить любую другую функцию.