Манипулирование DOM с помощью Refs¶
React автоматически обновляет DOM в соответствии с вашим выводом на экран, поэтому вашим компонентам не часто требуется манипулировать им. Однако иногда вам может понадобиться доступ к элементам DOM, управляемым React - например, для фокусировки узла, прокрутки к нему или измерения его размера и положения. Встроенного способа сделать это в React нет, поэтому вам понадобится ref на узел DOM.
Вы узнаете
- Как получить доступ к узлу DOM, управляемому React, с помощью атрибута
ref
- Как JSX-атрибут
ref
связан с хукомuseRef
- Как получить доступ к DOM-узлу другого компонента
- В каких случаях безопасно изменять DOM под управлением React
Получение ссылки на узел¶
Чтобы получить доступ к узлу DOM, управляемому React, сначала импортируйте хук useRef
:
1 |
|
Затем используйте его для объявления ссылки внутри вашего компонента:
1 |
|
Наконец, передайте его узлу DOM в качестве атрибута ref
:
1 |
|
Хук useRef
возвращает объект с единственным свойством current
. Изначально myRef.current
будет null
. Когда React создаст DOM-узел для этого <div>
, React поместит ссылку на этот узел в myRef.current
. Затем вы сможете обращаться к этому узлу DOM из ваших обработчиков событий и использовать встроенные API браузера, определенные на нем.
1 2 |
|
Пример: Фокусировка текстового ввода¶
В этом примере нажатие на кнопку фокусирует ввод:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
Чтобы реализовать это:
- Объявите
inputRef
с помощью хукаuseRef
. - Передайте его как
<input ref={inputRef}>
. Это говорит React ввести DOM-узел этого<input>
вinputRef.current
. - В функции
handleClick
прочитайте входной DOM-узел изinputRef.current
и вызовитеfocus()
на нем с помощьюinputRef.current.focus()
. - Передайте обработчик события
handleClick
в<button>
с помощьюonClick
.
Хотя манипуляции с DOM являются наиболее распространенным случаем использования ссылок, хук useRef
можно использовать для хранения других вещей вне React, например, идентификаторов таймеров. Аналогично состоянию, ссылки остаются между рендерами. Ссылки похожи на переменные состояния, которые не вызывают повторных рендеров, когда вы их устанавливаете. Читайте о реферерах в Ссылка на значения с помощью Refs.
Пример: Прокрутка к элементу¶
В компоненте может быть более одного элемента. В этом примере имеется карусель из трех изображений. Каждая кнопка центрирует изображение, вызывая метод браузера scrollIntoView()
на соответствующем узле DOM:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 |
|
Как управлять списком ссылок с помощью обратного вызова
В приведенных выше примерах существует предопределенное количество ссылок. Однако иногда вам может понадобиться ссылка на каждый элемент списка, и вы не знаете, сколько их будет. Что-то вроде этого не будет работать:
1 2 3 4 5 6 7 |
|
Это происходит потому, что Хуки должны вызываться только на верхнем уровне вашего компонента. Вы не можете вызвать useRef
в цикле, в условии или внутри вызова map()
.
Один из возможных способов обойти это - получить единственную ссылку на их родительский элемент, а затем использовать методы манипуляции DOM, такие как querySelectorAll
, чтобы "найти" отдельные дочерние узлы из него. Однако это хрупкий метод и может сломаться, если структура DOM изменится.
Другое решение - передать функцию атрибуту ref
. Это называется ref
callback. React вызовет ваш ref callback с узлом DOM, когда придет время установить ref, и с null
, когда придет время очистить его. Это позволяет вам вести свой собственный массив или Map, и обращаться к любому ref по его индексу или какому-либо идентификатору.
В данном примере показано, как можно использовать этот подход для прокрутки к произвольному узлу в длинном списке:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 |
|
В этом примере itemsRef
не содержит ни одного узла DOM. Вместо этого он содержит Map от ID элемента к узлу DOM. (Ссылки могут содержать любые значения!) Обратный вызов ref
для каждого элемента списка заботится об обновлении карты:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
Это позволит вам впоследствии считывать отдельные узлы DOM из карты.
Доступ к узлам DOM другого компонента¶
Когда вы помещаете ссылку на встроенный компонент, который выводит элемент браузера, такой как <input />
, React установит свойство current
этой ссылки на соответствующий узел DOM (такой как фактический <input />
в браузере).
Однако если вы попытаетесь поместить ссылку на свой собственный компонент, например <MyInput />
, по умолчанию вы получите null
. Вот пример, демонстрирующий это. Обратите внимание, что нажатие на кнопку не фокусирует ввод:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
|
Чтобы помочь вам заметить проблему, React также выводит ошибку в консоль:
Console
Warning: Function components cannot be given refs.
Attempts to access this ref will fail.
Did you mean to use React.forwardRef()?
Это происходит потому, что по умолчанию React не позволяет компоненту обращаться к узлам DOM других компонентов. Даже своим собственным детям! Это намеренно. Ссылки - это аварийный люк, который следует использовать очень редко. Ручное манипулирование DOM-узлами другого компонента делает ваш код еще более хрупким.
Вместо этого, компоненты, которые хотят раскрыть свои узлы DOM, должны оптировать такое поведение. Компонент может указать, что он "переадресует" свою ссылку одному из своих дочерних компонентов. Вот как MyInput
может использовать API forwardRef
:
1 2 3 |
|
Вот как это работает:
<MyInput ref={inputRef} />
говорит React поместить соответствующий узел DOM вinputRef.current
. Однако компонентMyInput
должен сам принять это решение - по умолчанию он этого не делает.- Компонент
MyInput
объявлен с использованиемforwardRef
. Это позволяет ему получатьinputRef
сверху в качестве второго аргументаref
, который объявляется послеprops
. - Сам
MyInput
передает полученныйref
в<input>
внутри него.
Теперь нажатие на кнопку для фокусировки ввода работает:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
|
В системах проектирования обычным шаблоном для низкоуровневых компонентов, таких как кнопки, входы и т.д., является передача ссылок на их узлы DOM. С другой стороны, высокоуровневые компоненты, такие как формы, списки или разделы страницы, обычно не раскрывают свои узлы DOM, чтобы избежать случайных зависимостей от структуры DOM.
Раскрытие подмножества API с помощью императивного дескриптора
В приведенном выше примере MyInput
раскрывает исходный элемент ввода DOM. Это позволяет родительскому компоненту вызывать focus()
на нем. Однако это также позволяет родительскому компоненту делать что-то еще - например, изменять стили CSS. В редких случаях вы можете захотеть ограничить открытую функциональность. Вы можете сделать это с помощью useImperativeHandle
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
|
Здесь realInputRef
внутри MyInput
содержит фактический входной DOM-узел. Однако useImperativeHandle
инструктирует React предоставлять ваш собственный специальный объект в качестве значения ссылки на родительский компонент. Таким образом, inputRef.current
внутри компонента Form
будет иметь только метод focus
. В этом случае ref "handle" - это не узел DOM, а пользовательский объект, который вы создаете внутри вызова useImperativeHandle
.
Когда React присоединяет рефы¶
В React каждое обновление делится на две фазы:
- Во время render, React вызывает ваши компоненты, чтобы выяснить, что должно быть на экране.
- Во время commit, React применяет изменения в DOM.
В общем, вы не хотите обращаться к рефкам во время рендеринга. Это относится и к ссылкам, содержащим узлы DOM. Во время первого рендеринга узлы DOM еще не были созданы, поэтому ref.current
будет null
. А во время рендеринга обновлений, узлы DOM еще не были обновлены. Поэтому читать их еще рано.
React устанавливает ref.current
во время фиксации. Перед обновлением DOM, React устанавливает затронутые значения ref.current
в null
. После обновления DOM, React немедленно устанавливает их в соответствующие узлы DOM.
Обычно вы обращаетесь к рефкам из обработчиков событий. Если вы хотите что-то сделать с рефкой, но нет конкретного события, в котором это можно сделать, вам может понадобиться эффект. Мы обсудим эффекты на следующих страницах.
Промывка обновлений состояния синхронно с помощью flushSync
Рассмотрим код, подобный этому, который добавляет новый todo и прокручивает экран вниз до последнего дочернего элемента списка. Обратите внимание, что по какой-то причине он всегда прокручивается к тому todo, который был прямо перед последним добавленным:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
|
Проблема заключается в этих двух линиях:
1 2 |
|
В React обновления состояния ставятся в очередь. Обычно это то, что вам нужно. Однако здесь это вызывает проблему, потому что setTodos
не обновляет DOM немедленно. Поэтому, когда вы прокручиваете список до последнего элемента, todo еще не был добавлен. Поэтому прокрутка всегда "отстает" на один элемент.
Чтобы решить эту проблему, вы можете заставить React обновлять ("промывать") DOM синхронно. Для этого импортируйте flushSync
из react-dom
и оберните обновление состояния в вызов flushSync
:
1 2 3 4 |
|
Это даст команду React синхронно обновить DOM сразу после выполнения кода, обернутого в flushSync
. В результате, последний todo уже будет в DOM к тому моменту, когда вы попытаетесь перейти к нему:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
|
Лучшие практики работы с DOM с помощью ссылок¶
Ссылки - это аварийный люк. Вы должны использовать их только тогда, когда вам нужно "выйти за пределы React". Обычные примеры этого - управление фокусом, позицией прокрутки или вызов API браузера, которые React не раскрывает.
Если вы придерживаетесь неразрушающих действий, таких как фокусировка и прокрутка, вы не должны столкнуться с какими-либо проблемами. Однако, если вы попытаетесь изменить DOM вручную, вы рискуете вступить в конфликт с изменениями, которые вносит React.
Чтобы проиллюстрировать эту проблему, данный пример включает в себя приветственное сообщение и две кнопки. Первая кнопка переключает свое присутствие, используя условный рендеринг и состояние, как вы обычно делаете в React. Вторая кнопка использует remove()
DOM API, чтобы принудительно удалить ее из DOM вне контроля React.
Попробуйте нажать "Toggle with setState" несколько раз. Сообщение должно исчезнуть и появиться снова. Затем нажмите "Удалить из DOM". Это приведет к принудительному удалению. Наконец, нажмите "Toggle with setState":
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
|
После того, как вы вручную удалили элемент DOM, попытка использовать setState
, чтобы снова показать его, приведет к сбою. Это происходит потому, что вы изменили DOM, и React не знает, как продолжать управлять им правильно.
Избегайте изменения узлов DOM, управляемых React. Изменение, добавление дочерних элементов или удаление дочерних элементов из элементов, управляемых React, может привести к непоследовательным визуальным результатам или сбоям, как описано выше.
Однако это не означает, что этого нельзя делать вообще. Это требует осторожности. Вы можете безопасно изменять части DOM, которые у React нет причин обновлять. Например, если какой-то <div>
всегда пуст в JSX, у React не будет причин трогать его список детей. Поэтому безопасно вручную добавлять или удалять там элементы.
Итоги
- Refs - это общая концепция, но чаще всего вы будете использовать их для хранения элементов DOM.
- Вы даете команду React поместить узел DOM в
myRef.current
, передавая<div ref={myRef}>
. - Обычно вы используете refs для неразрушающих действий, таких как фокусировка, прокрутка или измерение элементов DOM.
- Компонент по умолчанию не раскрывает свои DOM-узлы. Вы можете раскрыть узел DOM, используя
forwardRef
и передавая второй аргументref
вниз к определенному узлу. - Избегайте изменения узлов DOM, управляемых React.
- Если вы изменяете узлы DOM, управляемые React, изменяйте те части, которые React не имеет причин обновлять.
Задачи¶
1. Воспроизведение и пауза видео¶
В этом примере кнопка переключает переменную состояния для перехода между воспроизведением и паузой. Однако для того, чтобы действительно воспроизвести или поставить видео на паузу, переключения состояния недостаточно. Вам также необходимо вызвать play()
и pause()
на элементе DOM для <video>
. Добавьте к нему ссылку и заставьте кнопку работать.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
|
Для решения дополнительной задачи синхронизируйте кнопку "Play" с тем, воспроизводится ли видео, даже если пользователь щелкает правой кнопкой мыши на видео и воспроизводит его с помощью встроенных элементов управления мультимедиа браузера. Для этого вам может понадобиться прослушать onPlay
и onPause
на видео.
Показать решение
Объявите ссылку и поместите ее на элемент <video>
. Затем вызовите ref.current.play()
и ref.current.pause()
в обработчике событий в зависимости от следующего состояния.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
|
Для работы со встроенными элементами управления браузера вы можете добавить обработчики onPlay
и onPause
к элементу <video>
и вызвать из них setIsPlaying
. Таким образом, если пользователь воспроизводит видео с помощью элементов управления браузера, состояние будет соответствующим образом изменено.
2. Фокусировка поля поиска¶
Сделайте так, чтобы нажатие на кнопку "Поиск" наводило фокус на поле.
1 2 3 4 5 6 7 8 9 10 |
|
Показать решение
Добавьте ссылку на вход и вызовите focus()
на узле DOM, чтобы сфокусировать его:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
|
3. Прокрутка карусели изображений¶
Эта карусель изображений имеет кнопку "Next", которая переключает активное изображение. Заставьте галерею прокручиваться горизонтально до активного изображения по щелчку. Для этого нужно вызвать scrollIntoView()
на DOM-узле активного изображения:
1 2 3 4 5 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
|
Показать подсказку
Для этого упражнения не обязательно иметь ссылку на каждое изображение. Достаточно иметь ссылку на текущее активное изображение или на сам список. Используйте flushSync
для обеспечения обновления DOM до прокрутки.
Показать решение
Вы можете объявить selectedRef
, а затем передать его условно только текущему изображению:
1 |
|
Когда index === i
, что означает, что изображение является выбранным, <li>
получит selectedRef
. React будет следить за тем, чтобы selectedRef.current
всегда указывал на правильный узел DOM.
Обратите внимание, что вызов flushSync
необходим для того, чтобы заставить React обновить DOM перед прокруткой. В противном случае selectedRef.current
всегда будет указывать на ранее выбранный элемент.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 |
|
4. Фокусировка поля поиска с помощью отдельных компонентов¶
Сделайте так, чтобы нажатие на кнопку "Поиск" наводило фокус на поле. Обратите внимание, что каждый компонент определен в отдельном файле и не должен быть перемещен из него. Как соединить их вместе?
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
1 2 3 |
|
1 2 3 |
|
Показать подсказку
Вам понадобится forwardRef
, чтобы открыть узел DOM из вашего собственного компонента, такого как SearchInput
.
Показать решение
Вам нужно добавить свойство onClick
к SearchButton
и заставить SearchButton
передать его браузеру <button>
. Вы также передадите ссылку в <SearchInput>
, который передаст ее в настоящий <input>
и заполнит его. Наконец, в обработчике клика вы вызовете focus
для узла DOM, хранящегося внутри ссылки.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
1 2 3 |
|
1 2 3 4 5 6 7 8 9 10 |
|
Источник — https://react.dev/learn/manipulating-the-dom-with-refs