Привет Хабр!
Как вы знаете при переходе с компонентов классов на
функциональные, у нас отняли такую полезную вещь как this, которая
указывает на текущий экземпляр компонента. И конечно у нас возник
вопрос: а где же тогда хранить timeoutId
?. И я видел
как люди по разному выкручивались из этой проблемы (Данная статья, является
расшифровкой видео)
Например, если timeoutId
используется только в
рамках одного useEffect
можно набросать следующий
вариант:
useEffect(() => { const timeout = setTimeout(() => { // do some action }, 3000); return () => { clearTimeout(timeout); }}, [...]);
Данный подход работает, но когда появляется нужда очищать
timeout
по клику на кнопку, этот подход уже не
работает.
Поэтому многие решили, просто создавать переменную вне
компонента и хранить в ней id
этого тайм-аута, чтобы
например по клику иметь возможность его отменить:
let timeout;const Test = () => { const onClick = () => clearTimeout(timeout); useEffect(() => { timeout = setTimeout(() => { // do some action }, 3000); }, [...]); return (...);}
И это работает в большинстве случаев без каких-либо проблем. Но как всегда есть НО.
Проблема глобальных переменных
Давайте рассмотрим пример. Допустим у нас есть компонент Counter, в котором есть локальный счетчик и глобальный счетчик определенный вне компонента:
let globalCounter = 0;const Counter = () => { const [stateCounter, setStateCounter] = useState(0); const onClick = () => { globalCounter++; setStateCounter((stateCounter) => stateCounter + 1); }; return ( <div> <p>global counter - <b>{globalCounter}</b></p> <p>state counter - <b>{stateCounter}</b></p> <button onClick={onClick}>increment</button> </div> );}
Компонент достаточно простой. Теперь добавим родительский компонент:
const App = () => { const [countersNumber, setCountersNumber] = useState(0); return ( <div> <button onClick={setCountersNumber((count) => count + 1)}> add </button> <button onClick={setCountersNumber((count) => count - 1)}> removed </button> {[...Array.from(countersNumber).keys()].map((index) => ( <Counter key={index} /> ))} </div> );};
Здесь мы храним в state
количество счетчиков, и
ниже имеем 2 кнопки: для увеличения количества счетчиков и для
уменьшения. И собственно вставляем сами счетчики в таком
количество, как у нас указано в переменной
countersNumber
.
Смотрим результат
Перейдем в браузер и выполним следующие действия:
-
Добавим один счетчик;
-
Внутри появившегося счетчика, нажмем "increment" три раза;
-
Добавим второй счетчик.
Как вы видите у второго глобального каунтера значение три. И не важно сколько счетчиков вы создадите, они все замыкаются на одну и ту же глобальную переменную. И даже если вы удалите все счетчики, а потом добавите новый счетчик, конечно же глобальный каунтер все равно будет равен трем, так как глобальная переменная создается при запуске сайта и очищается при полном закрытии табы в браузере с вашим сайтом.
Таким образом, хранение timeoutId
в такого рода
глобальной переменной, может привести к странным багам. Да и если у
вас в проекте огромное количество таких переменных, вы бесполезно
засоряете память вашего компьютера, хоть это возможно и экономия на
спичках.
Рассмотрим альтернативу
Решением данной проблемы является использование хука
useRef()
. Именно это и рекомендует React
документация:
Они прямо упомянули, что useRef()
нужно
использовать как аналог this
. И более того, для
удобства добавили в useRef()
возможность передачи
начального значения. Поэтому вариант с timeout
может
выглядеть следующим образом:
const Test = () => { const timeout = useRef(); const onClick = () => clearTimeout(timeout.current); useEffect(() => { timeout.current = setTimeout(() => { // do some action }, 3000); }, [...]); return (...);}
Возможно в этом решении вас смущает, то что в
timeout
начинает хранится свойство
current
, это действительно выглядит немного странно,
но у этого есть разумное объяснение, о котором мы рассказывали в
предыдущей статье createRef,
setRef, useRef и зачем нужен current в ref.
prevProps не исчезли вместе с классами
Использование useRef()
для хранения
timeout
это конечно же очень полезно. Но есть и более
интересные способы использования. Например, в компонентах в виде
классов есть удобный метод жизненного цикла
componentDidUpdate
. В качестве первого параметра нам
предоставляют prevProps
, т.е. props
из
предыдущей итерации. Это давало нам возможность, сравнивать
props
из текущей итерации с props
из
предыдущей. На основании этого выполнять какие-то действия.
componentDidUpdate(prevProps) { if (this.props.id !== prevProps.id) { // do some action }}
Если вам кажется, что эту функциональность у вас отняли безвозвратно, тогда вы ошибаетесь.
Давайте напишем хук, который будет возвращать props
из предыдущей итерации:
const useGetPrevValue = (value) => { const prevValueRef = useRef(); useEffect(() => { prevValueRef.current = value; }); return prevValueRef.current;};
Здесь мы получаем value
из текущей итерации, после
создадим ref
для хранения данных между итерациями. И в
рамках текущей итерации мы вернем текущее значение
current
равное null
. Но перед началом
следующей итерации мы обновим current
значение, таким
образом в следующей итерации в ref
у нас будет
хранится значение из предыдущей.
И осталось только использовать этот хук:
const CounterView = ({ counter }) => { const prevCount = useGetPrevValue(counter); const classes = classNames({ [styles.greenCounter]: counter < prevCounter, [styles.redCounter] counter > prevCounter, }); ...}
В итоге, имея значение counter
из предыдущей
итерации и из текущей итерации, мы можем покрасить в зеленый цвет,
если counter
уменьшился, или в красный, если
counter
увеличился.
Расширяйте сознание
Мы упомянули лишь несколько базовых вариантов использования
ref
для решения нетипичных для ref
задач.
Но их гораздо больше. Например, если вы откроете телеграм десктоп
версии, и будете скролить вверх, то у вас будет отображаться дата,
а если вниз, она исчезнет. Мы можем хранить предыдущее значение
скрола в ref
, для того чтобы вычислить в какую же
сторону скролит пользователь.
Как вы уже догадались, использование ref
ограничено
лишь вашей фантазией и вы найдете ему гораздо больше вариантов
применения, чем мы упомянули в этой статье.
И если вы знаете еще какие-то интересные варианты использования
ref
обязательно пишите в комментариях