Ссылки на значения с помощью refs
Если вам нужно, чтобы компонент «запомнил» какую-то информацию, но при этом вы не хотите, чтобы эта информация вызывала новую прорисовку, вы можете использовать *ref*.
Вы узнаете
- Как добавить ref в компонент
- Как обновлять значение ref
- Чем refs отличаются от state
- Как безопасно использовать refs
Добавление ref в ваш компонент
Вы можете добавить ref в свой компонент, импортировав хук useRef из React:
import { useRef } from 'react';Внутри вашего компонента вызовите хук useRef и передайте в качестве единственного аргумента начальное значение, на которое вы хотите сослаться. Например, вот ref на значение 0:
const ref = useRef(0);useRef возвращает объект, подобный этому:
{
current: 0 // Значение, которое вы передали в useRef
}
Вы можете получить доступ к текущему значению этого рефа через свойство ref.current. Это значение намеренно сделано изменяемым, то есть вы можете как читать его, так и записывать в него. Это как секретный карман вашего компонента, который React не отслеживает. (Именно это делает его «лазейкой» из однонаправленного потока данных React — подробнее об этом ниже!)
Здесь кнопка будет увеличивать значение ref.current при каждом нажатии:
import { useRef } from 'react';export default function Counter() { let ref = useRef(0); function handleClick() { ref.current = ref.current + 1; alert('You clicked ' + ref.current + ' times!'); } return ( <button onClick={handleClick}> Click me! </button> );}Ref указывает на число, но, как и state, вы можете указывать на что угодно: строку, объект или даже функцию. В отличие от state, ref — это простой объект JavaScript со свойством current, которое вы можете читать и изменять.
Обратите внимание, что компонент не перерисовывается при каждом изменении значения ref. Как и state, ref сохраняются React между перерисовками. Однако установка state приводит к перерисовке компонента. Изменение ref — нет!
Пример: создание секундомера
Вы можете комбинировать ref и state в одном компоненте. Например, давайте создадим секундомер, который пользователь может запустить или остановить нажатием кнопки. Чтобы отобразить, сколько времени прошло с момента нажатия кнопки «Start», вам нужно будет отслеживать, когда была нажата кнопка «Start» и каково текущее время. Эта информация используется для рендеринга, поэтому вы будете хранить её в state:
const [startTime, setStartTime] = useState(null);
const [now, setNow] = useState(null);Когда пользователь нажмет «Start», вы будете использовать setInterval, чтобы обновлять время каждые 10 миллисекунд:
import { useState } from 'react';export default function Stopwatch() { const [startTime, setStartTime] = useState(null); const [now, setNow] = useState(null); function handleStart() { // Start counting. setStartTime(Date.now()); setNow(Date.now()); setInterval(() => { // Update the current time every 10ms. setNow(Date.now()); }, 10); } let secondsPassed = 0; if (startTime != null && now != null) { secondsPassed = (now - startTime) / 1000; } return ( <> <h1>Time passed: {secondsPassed.toFixed(3)}</h1> <button onClick={handleStart}> Start </button> </> );}При нажатии кнопки «Стоп» необходимо отменить существующий интервал, чтобы он перестал обновлять переменную состояния now. Это можно сделать, вызвав clearInterval, но необходимо передать ему ID интервала, который ранее был возвращен вызовом setInterval, когда пользователь нажал «Старт». Вам нужно где-то сохранить идентификатор интервала. Поскольку идентификатор интервала не используется для рендеринга, вы можете сохранить его в ref:
import { useState, useRef } from 'react';export default function Stopwatch() { const [startTime, setStartTime] = useState(null); const [now, setNow] = useState(null); const intervalRef = useRef(null); function handleStart() { setStartTime(Date.now()); setNow(Date.now()); clearInterval(intervalRef.current); intervalRef.current = setInterval(() => { setNow(Date.now()); }, 10); } function handleStop() { clearInterval(intervalRef.current); } let secondsPassed = 0; if (startTime != null && now != null) { secondsPassed = (now - startTime) / 1000; } return ( <> <h1>Time passed: {secondsPassed.toFixed(3)}</h1> <button onClick={handleStart}> Start </button> <button onClick={handleStop}> Stop </button> </> );}Если какая-то информация используется для рендеринга, храните её в state. Если же информация нужна только обработчикам событий и её изменение не требует повторного рендеринга, использование ref может быть более эффективным.
Различия между refs и state
Возможно, вам кажется, что refs менее «строгие», чем state — например, вы можете изменять их, вместо того чтобы всегда использовать функцию установки состояния. Но в большинстве случаев вам лучше использовать state. Refs — это «аварийный выход», который вам не понадобится часто. Вот сравнение state и refs:
| refs | state |
|---|---|
useRef(initialValue) возвращает { current: initialValue } | useState(initialValue) возвращает текущее значение переменной состояния и функцию установки состояния ( [value, setValue]) |
| Не вызывает повторный рендеринг при изменении. | Вызывает повторный рендеринг при изменении. |
Изменяемое — вы можете изменять и обновлять значение current вне процесса рендеринга. | «Неизменяемое» — вы должны использовать функцию установки состояния для изменения переменных состояния, чтобы инициировать повторный рендеринг. |
Вы не должны считывать (или записывать) значение current во время рендеринга. | Вы можете считывать состояние в любое время. Однако каждый рендеринг имеет свой собственный снэпшот состояния, который не изменяется. |
Вот кнопка счетчика, реализованная с использованием состояния:
import { useState } from 'react';export default function Counter() { const [count, setCount] = useState(0); function handleClick() { setCount(count + 1); } return ( <button onClick={handleClick}> You clicked {count} times </button> );}Поскольку значение count отображается, имеет смысл использовать для него значение состояния. Когда значение счетчика устанавливается с помощью setCount(), React перерисовывает компонент, и экран обновляется, отражая новый счет.
Если бы вы попытались реализовать это с помощью ref, React никогда бы не перерисовал компонент, поэтому вы бы никогда не увидели изменения счета! Посмотрите, как нажатие этой кнопки не обновляет ее текст:
import { useRef } from 'react';export default function Counter() { let countRef = useRef(0); function handleClick() { // This doesn't re-render the component! countRef.current = countRef.current + 1; } return ( <button onClick={handleClick}> You clicked {countRef.current} times </button> );}Вот почему чтение ref.current во время рендеринга приводит к ненадежному коду. Если вам это нужно, используйте вместо этого state.
Как работает useRef изнутри?
Хотя и useState, и useRef предоставляются React, в принципе useRef можно было бы реализовать на основе useState. Можно представить, что внутри React useRef реализован следующим образом:
// Внутри React
function useRef(initialValue) {
const [ref, unused] = useState({ current: initialValue });
return ref;
}Во время первого рендеринга useRef возвращает { current: initialValue }. Этот объект сохраняется React, поэтому во время следующего рендеринга будет возвращен тот же объект.
Обратите внимание, что в этом примере сеттер состояния не используется. Он не нужен, потому что useRef всегда должен возвращать один и тот же объект!
React предоставляет встроенную версию useRef, поскольку на практике она достаточно распространена. Но вы можете рассматривать ее как обычную переменную состояния без сеттера. Если вы знакомы с объектно-ориентированным программированием, рефы могут напомнить вам поля экземпляра — но вместо this.something вы пишете somethingRef.current.
Когда использовать рефы
Обычно вы будете использовать ref, когда вашему компоненту нужно «выйти за пределы» React и взаимодействовать с внешними API — часто с API браузера, которые не повлияют на внешний вид компонента. Вот несколько таких редких ситуаций:
- Хранение идентификаторов таймаутов
- Хранение и манипулирование элементами DOM, что мы рассмотрим на следующей странице
- Хранение других объектов, которые не нужны для вычисления JSX. Если вашему компоненту нужно сохранить какое-то значение, но это не влияет на логику рендеринга, выбирайте refs.
Рекомендации по использованию refs
Соблюдение этих принципов сделает ваши компоненты более предсказуемыми:
- Рассматривайте refs как запасной выход. Refs полезны при работе с внешними системами или API браузера. Если большая часть логики вашего приложения и потока данных зависит от refs, вам, возможно, стоит пересмотреть свой подход.
- Не читайте и не записывайте
ref.currentво время рендеринга. Если какая-то информация нужна во время рендеринга, используйте вместо этого state. Поскольку React не знает, когда изменяетсяref.current, даже чтение этого значения во время рендеринга делает поведение вашего компонента труднопредсказуемым. (Единственным исключением из этого правила является код типаif (!ref.current) ref.current = new Thing(), который устанавливает ref только один раз во время первого рендеринга.) Ограничения состояния React не распространяются на рефы. Например, состояние действует как снимок для каждого рендеринга и не обновляется синхронно. Но когда вы изменяете текущее значение рефа, оно меняется немедленно:
ref.current = 5;
console.log(ref.current); // 5Это происходит потому, что сам ref является обычным объектом JavaScript, и поэтому ведет себя как таковой.
Вам также не нужно беспокоиться о предотвращении мутации при работе с ref. Пока объект, который вы изменяете, не используется для рендеринга, React не заботится о том, что вы делаете с ref или его содержимым.
Refs и DOM
Вы можете направить ref на любое значение. Однако наиболее распространенным случаем использования ref является доступ к элементу DOM. Например, это удобно, если вы хотите программно выделить поле ввода. Когда вы передаете ref в атрибут ref в JSX, например <div ref={myRef}>, React поместит соответствующий элемент DOM в myRef.current. Как только элемент будет удален из DOM, React обновит myRef.current, присвоив ему значение null. Вы можете узнать об этом больше в Работа с DOM с помощью реф.
Вывод
- Рефы — это запасной вариант для сохранения значений, которые не используются при рендеринге. Они вам не понадобятся часто.
- Реф — это простой объект JavaScript с единственным свойством
current, которое можно читать или устанавливать. - Вы можете попросить React предоставить вам ref, вызвав хук
useRef. - Как и state, refs позволяют сохранять информацию между перерисовками компонента.
- В отличие от state, установка значения
currentref не вызывает перерисовку. - Не читайте и не записывайте
ref.currentво время рендеринга. Это делает ваш компонент труднопредсказуемым.
Escape Hatches
Некоторым вашим компонентам может понадобиться управлять системами вне React и синхронизироваться с ними. Например, вам может понадобиться установить фокус на input с помощью browser API, запускать и ставить на паузу видеоплеер, реализованный без React, или подключаться к удалённому серверу и слушать сообщения от него. В этой главе вы познакомитесь с escape hatches, которые позволяют "выйти" за пределы React и подключаться к внешним системам. Большая часть логики приложения и потоков данных не должна опираться на эти возможности.
Манипулирование DOM с помощью ref
React автоматически обновляет DOM, чтобы он соответствовал результату рендера, поэтому компонентам обычно не нужно управлять им вручную. Однако иногда может потребоваться доступ к DOM-элементам, которыми управляет React — например, чтобы перевести фокус на узел, прокрутить к нему страницу или измерить его размер и положение. В React нет встроенного способа делать такие вещи, поэтому вам понадобится *ref* для DOM-узла.