Обработка DOM с помощью Refs
React автоматически обновляет DOM в соответствии с результатом рендеринга, поэтому компонентам часто не нужно его изменять. Однако иногда вам может потребоваться доступ к элементам DOM, управляемым React, например, для фокусировки узла, прокрутки к нему или измерения его размера и положения. В React нет встроенного способа сделать это, поэтому вам потребуется ссылка на узел DOM.
Вы узнаете
- Как получить доступ к узлу DOM, управляемому React, с помощью атрибута
ref - Как атрибут
refJSX связан с хукомuseRef - Как получить доступ к узлу DOM другого компонента
- В каких случаях безопасно изменять DOM, управляемый React
Получение ссылки на узел
Чтобы получить доступ к узлу DOM, управляемому React, сначала импортируйте хук useRef:
import { useRef } from 'react'; Затем используйте его для объявления ссылки внутри вашего компонента:
const myRef = useRef(null); Наконец, передайте вашу ссылку в качестве атрибута ref тегу JSX, для которого вы хотите получить узел DOM:
<div ref={myRef}> Хук useRef возвращает объект с единственным свойством, называемым current. Изначально myRef.current будет null. Когда React создает узел DOM для этого <div>, React поместит ссылку на этот узел в myRef.current. Затем вы можете получить доступ к этому узлу DOM из своих обработчиков событий и использовать встроенные обработчики событий и встроенные браузерные API, определенные на нём.
// You can use any browser APIs, for example:
myRef.current.scrollIntoView(); Пример: Фокусировка текстового поля
В этом примере нажатие на кнопку сфокусирует поле ввода:
import { useRef } from 'react'; export default function Form() { const inputRef = useRef(null); function handleClick() { inputRef.current.focus(); } return ( <> <input ref={inputRef} /> <button onClick={handleClick}> Focus the input </button> </> ); }
Для реализации этого:
- Объявите
inputRefс помощью хукаuseRef. - Передайте его в качестве
<input ref={inputRef}>. Это сообщает React, что нужно поместить узел DOM этого<input>вinputRef.current. - В функции
handleClick, прочтите узел DOM элемента ввода изinputRef.currentи вызовитеfocus()на нём с помощьюinputRef.current.focus(). - Передайте обработчик события
handleClickэлементу<button>с помощьюonClick.
Хотя манипулирование DOM является наиболее распространенным случаем использования ссылок, хук useRef можно использовать для хранения других вещей вне React, например, идентификаторов таймеров. Аналогично состоянию, ссылки сохраняются между рендерами. Ссылки похожи на переменные состояния, которые не вызывают повторное рендеринг, когда вы их устанавливаете. Подробнее о ссылках читайте в Ссылках для присвоения значений.
Пример: Прокрутка к элементу
В компоненте может быть более одной ссылки. В этом примере есть карусель из трех изображений. Каждая кнопка центрирует изображение, вызывая браузерный метод scrollIntoView() на соответствующем узле DOM:
import { useRef } from 'react'; export default function CatFriends() { const firstCatRef = useRef(null); const secondCatRef = useRef(null); const thirdCatRef = useRef(null); function handleScrollToFirstCat() { firstCatRef.current.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'center' }); } function handleScrollToSecondCat() { secondCatRef.current.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'center' }); } function handleScrollToThirdCat() { thirdCatRef.current.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'center' }); } return ( <> <nav> <button onClick={handleScrollToFirstCat}> Neo </button> <button onClick={handleScrollToSecondCat}> Millie </button> <button onClick={handleScrollToThirdCat}> Bella </button> </nav> <div> <ul> <li> <img src="https://placecats.com/neo/300/200" alt="Neo" ref={firstCatRef} /> </li> <li> <img src="https://placecats.com/millie/200/200" alt="Millie" ref={secondCatRef} /> </li> <li> <img src="https://placecats.com/bella/199/200" alt="Bella" ref={thirdCatRef} /> </li> </ul> </div> </> ); }
Подробный разбор
Как управлять списком ссылок с помощью обратного вызова ссылки
В приведенных выше примерах количество ссылок определено заранее. Однако иногда вам может потребоваться ссылка на каждый элемент списка, и вы не знаете, сколько их будет. Такой код не будет работать:
<ul>
{items.map((item) => {
// Doesn't work!
const ref = useRef(null);
return <li ref={ref} />;
})}
</ul>
Это происходит потому, что хуки должны вызываться только на верхнем уровне вашего компонента. Вы не можете вызвать useRef в цикле, в условии или внутри вызова map().
Один из возможных способов обойти это — получить единственную ссылку на родительский элемент, а затем использовать методы манипулирования DOM, такие как querySelectorAll, чтобы «найти» отдельные дочерние узлы из него. Однако это хрупко и может сломаться, если структура вашего DOM изменится.
Другое решение — передать функцию атрибуту ref. Это называется ref обратный вызов. React вызовет ваш обратный вызов ссылки с узлом DOM, когда придет время установить ссылку, и с null, когда придет время его очистить. Это позволяет вам поддерживать свой собственный массив или Map, и получать доступ к любой ссылке по ее индексу или какому-либо идентификатору.
В этом примере показано, как использовать этот подход для прокрутки к произвольному узлу в длинном списке:
import { useRef, useState } from "react"; export default function CatFriends() { const itemsRef = useRef(null); const [catList, setCatList] = useState(setupCatList); function scrollToCat(cat) { const map = getMap(); const node = map.get(cat); node.scrollIntoView({ behavior: "smooth", block: "nearest", inline: "center", }); } function getMap() { if (!itemsRef.current) { // Initialize the Map on first usage. itemsRef.current = new Map(); } return itemsRef.current; } return ( <> <nav> <button onClick={() => scrollToCat(catList[0])}>Neo</button> <button onClick={() => scrollToCat(catList[5])}>Millie</button> <button onClick={() => scrollToCat(catList[9])}>Bella</button> </nav> <div> <ul> {catList.map((cat) => ( <li key={cat} ref={(node) => { const map = getMap(); map.set(cat, node); return () => { map.delete(cat); }; }} > <img src={cat} /> </li> ))} </ul> </div> </> ); } function setupCatList() { const catList = []; for (let i = 0; i < 10; i++) { catList.push("https://loremflickr.com/320/240/cat?lock=" + i); } return catList; }
В этом примере itemsRef не содержит единственного узла DOM. Вместо этого он содержит Map из идентификатора элемента до узла DOM. (Ссылки могут содержать любые значения!) Обратный вызов ref на каждом элементе списка заботится об обновлении Map:
<li
key={cat.id}
ref={node => {
const map = getMap();
// Add to the Map
map.set(cat, node);
return () => {
// Remove from the Map
map.delete(cat);
};
}}
>
Это позволяет позже считывать отдельные узлы DOM из Map.
Примечание
При включенном режиме Strict Mode обратные вызовы ссылок будут выполняться дважды в режиме разработки.
Узнайте больше о том, как это помогает найти ошибки в обратных вызовах ссылок.
Получение доступа к узлам DOM другого компонента
Когда вы помещаете ссылку на встроенный компонент, который выводит браузерный элемент, как <input />, React установит свойство current этой ссылки на соответствующий узел DOM (например, фактический <input /> в браузере).
Однако, если вы попытаетесь поместить ссылку на ваш собственный компонент, как <MyInput />, по умолчанию вы получите null. Вот пример, демонстрирующий это. Обратите внимание, как нажатие на кнопку не фокусирует поле ввода:
import { useRef } from 'react'; function MyInput(props) { return <input {...props} />; } export default function MyForm() { const inputRef = useRef(null); function handleClick() { inputRef.current.focus(); } return ( <> <MyInput ref={inputRef} /> <button onClick={handleClick}> Focus the input </button> </> ); }
Чтобы помочь вам заметить проблему, React также выводит ошибку в консоль:
Это происходит потому, что по умолчанию React не позволяет компоненту получить доступ к узлам DOM других компонентов. Даже не для собственных дочерних элементов! Это сделано намеренно. Ссылки — это аварийный выход, который следует использовать с осторожностью. Ручное манипулирование узлами DOM другого компонента делает ваш код еще более хрупким.
Вместо этого компоненты, которые хотят экспонировать свои узлы DOM, должны заявить об этом. Компонент может указать, что он «передает» свою ссылку одному из своих дочерних элементов. Вот как MyInput может использовать API forwardRef:
const MyInput = forwardRef((props, ref) => {
return <input {...props} ref={ref} />;
}); Вот как это работает:
-
<MyInput ref={inputRef} />сообщает React поместить соответствующий узел DOM вinputRef.current. Однако, за этим стоит компонентMyInput, который должен заявить об этом — по умолчанию он этого не делает. - Компонент
MyInputобъявляется с помощьюforwardRef. Это заставляет его приниматьinputRefиз выше как второйrefаргумент, который объявляется послеprops. -
Сам
MyInputпередает полученноеrefкомпоненту<input>внутри него.
Теперь нажатие на кнопку для фокусировки поля ввода работает:
import { forwardRef, useRef } from 'react'; const MyInput = forwardRef((props, ref) => { return <input {...props} ref={ref} />; }); export default function Form() { const inputRef = useRef(null); function handleClick() { inputRef.current.focus(); } return ( <> <MyInput ref={inputRef} /> <button onClick={handleClick}> Focus the input </button> </> ); }
В системах проектирования распространённым шаблоном является передача ссылок для низкоуровневых компонентов, таких как кнопки, поля ввода и так далее. С другой стороны, высокоуровневые компоненты, такие как формы, списки или разделы страниц, обычно не экспонируют свои узлы DOM, чтобы избежать случайных зависимостей от структуры DOM.
Подробный разбор
Экспонирование подмножества API с помощью императивного обработчика
В приведенном выше примере MyInput экспонирует исходный элемент DOM поля ввода. Это позволяет родительскому компоненту вызвать focus() на нём. Однако это также позволяет родительскому компоненту сделать что-то ещё — например, изменить его стили CSS. В редких случаях вам может потребоваться ограничить экспонируемую функциональность. Это можно сделать с помощью useImperativeHandle:
import { forwardRef, useRef, useImperativeHandle } from 'react'; const MyInput = forwardRef((props, ref) => { const realInputRef = useRef(null); useImperativeHandle(ref, () => ({ // Only expose focus and nothing else focus() { realInputRef.current.focus(); }, })); return <input {...props} ref={realInputRef} />; }); export default function Form() { const inputRef = useRef(null); function handleClick() { inputRef.current.focus(); } return ( <> <MyInput ref={inputRef} /> <button onClick={handleClick}> Focus the input </button> </> ); }
Здесь realInputRef внутри MyInput содержит фактический узел DOM поля ввода. Однако useImperativeHandle инструктирует React предоставить свой собственный специальный объект в качестве значения ссылки для родительского компонента. Таким образом, inputRef.current внутри компонента Form будет иметь только метод focus. В этом случае ссылка «обработчик» — это не узел DOM, а пользовательский объект, который вы создаёте внутри вызова useImperativeHandle.
Когда React прикрепляет ссылки
В React каждое обновление разделено на две фазы:
- Во время рендеринга React вызывает ваши компоненты, чтобы определить, что должно быть на экране.
- Во время коммита React применяет изменения к DOM.
В целом, вы не должны обращаться к ссылкам во время рендеринга. Это относится и к ссылкам, содержащим узлы DOM. Во время первого рендеринга узлы DOM ещё не созданы, поэтому ref.current будет null. А во время рендеринга обновлений узлы DOM ещё не обновлены. Поэтому слишком рано их читать.
React устанавливает ref.current во время коммита. Прежде чем обновить DOM, React устанавливает затронутые ref.current значения в null. После обновления DOM React немедленно устанавливает их на соответствующие узлы DOM.
Обычно вы будете получать доступ к ссылкам из обработчиков событий. Если вам нужно что-то сделать со ссылкой, но нет конкретного события, для этого может потребоваться эффект. Мы рассмотрим эффекты на следующих страницах.
Подробное изучение
Синхронное обновление состояния с помощью flushSync
Рассмотрим код, который добавляет новую задачу и прокручивает экран вниз до последнего элемента списка. Обратите внимание, что по какой-то причине он всегда прокручивается к задаче, которая предшествует последней добавленной:
import { useState, useRef } from 'react'; export default function TodoList() { const listRef = useRef(null); const [text, setText] = useState(''); const [todos, setTodos] = useState( initialTodos ); function handleAdd() { const newTodo = { id: nextId++, text: text }; setText(''); setTodos([ ...todos, newTodo]); listRef.current.lastChild.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); } return ( <> <button onClick={handleAdd}> Add </button> <input value={text} onChange={e => setText(e.target.value)} /> <ul ref={listRef}> {todos.map(todo => ( <li key={todo.id}>{todo.text}</li> ))} </ul> </> ); } let nextId = 0; let initialTodos = []; for (let i = 0; i < 20; i++) { initialTodos.push({ id: nextId++, text: 'Todo #' + (i + 1) }); }
Проблема кроется в этих двух строках:
setTodos([ ...todos, newTodo]);
listRef.current.lastChild.scrollIntoView();
В React обновления состояния очередируются. Обычно это то, что нужно. Однако в данном случае это приводит к проблеме, поскольку setTodos не обновляет DOM немедленно. Поэтому в момент, когда вы прокручиваете список до последнего элемента, задача ещё не добавлена. Вот почему прокрутка всегда отстаёт на один элемент.
Чтобы решить эту проблему, можно принудительно обновить («сбросить») DOM синхронно. Для этого импортируйте flushSync из react-dom и оберните обновление состояния в вызов flushSync:
flushSync(() => {
setTodos([ ...todos, newTodo]);
});
listRef.current.lastChild.scrollIntoView();
Это укажет React на синхронное обновление DOM сразу после выполнения кода, заключённого в flushSync. В результате последняя задача уже будет в DOM в момент попытки прокрутки к ней:
import { useState, useRef } from 'react'; import { flushSync } from 'react-dom'; export default function TodoList() { const listRef = useRef(null); const [text, setText] = useState(''); const [todos, setTodos] = useState( initialTodos ); function handleAdd() { const newTodo = { id: nextId++, text: text }; flushSync(() => { setText(''); setTodos([ ...todos, newTodo]); }); listRef.current.lastChild.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); } return ( <> <button onClick={handleAdd}> Add </button> <input value={text} onChange={e => setText(e.target.value)} /> <ul ref={listRef}> {todos.map(todo => ( <li key={todo.id}>{todo.text}</li> ))} </ul> </> ); } let nextId = 0; let initialTodos = []; for (let i = 0; i < 20; i++) { initialTodos.push({ id: nextId++, text: 'Todo #' + (i + 1) }); }
Рекомендации по работе с DOM с помощью refs
Refs — это возможность выйти за пределы React. Их следует использовать только тогда, когда нужно «выйти за пределы React». Общие примеры включают управление фокусом, положением прокрутки или вызов браузерных API, которые React не предоставляет.
Если вы ограничиваетесь ненаносящими изменений действиями, такими как фокусировка и прокрутка, проблем возникнуть не должно. Однако, если вы попытаетесь изменить DOM вручную, вы рискуете столкнуться с конфликтами с изменениями, которые вносит React.
Чтобы проиллюстрировать эту проблему, в данном примере представлено приветственное сообщение и две кнопки. Первая кнопка переключает своё отображение с помощью условного рендеринга и состояния, как обычно делается в React. Вторая кнопка использует remove() API DOM для принудительного удаления из DOM вне контроля React.
Попробуйте несколько раз нажать кнопку «Переключить с помощью setState». Сообщение должно исчезать и снова появляться. Затем нажмите «Удалить из DOM». Это принудительно удалит его. Наконец, нажмите «Переключить с помощью setState»:
import { useState, useRef } from 'react'; export default function Counter() { const [show, setShow] = useState(true); const ref = useRef(null); return ( <div> <button onClick={() => { setShow(!show); }}> Toggle with setState </button> <button onClick={() => { ref.current.remove(); }}> Remove from the DOM </button> {show && <p ref={ref}>Hello world</p>} </div> ); }
После того как вы вручную удалили элемент 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 из 4:
Воспроизведение и пауза видео
В данном примере кнопка переключает переменную состояния для переключения между состояниями воспроизведения и паузы. Однако для фактического воспроизведения или приостановки видео недостаточно переключения состояния. Вам также необходимо вызвать play() и pause() для элемента DOM видео. Добавьте ref к нему, и кнопка должна работать.
import { useState, useRef } from 'react'; export default function VideoPlayer() { const [isPlaying, setIsPlaying] = useState(false); function handleClick() { const nextIsPlaying = !isPlaying; setIsPlaying(nextIsPlaying); } return ( <> <button onClick={handleClick}> {isPlaying ? 'Pause' : 'Play'} </button> <video width="250"> <source src="https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4" type="video/mp4" /> </video> </> ) }
Для дополнительной задачи поддерживайте синхронизацию кнопки «Воспроизвести» с состоянием воспроизведения видео, даже если пользователь нажимает правую кнопку мыши на видео и запускает воспроизведение с помощью встроенных средств управления медиа браузера. Для этого можно прослушивать onPlay и onPause на видео.
© 2013–present Facebook Inc.
Licensed under the Creative Commons Attribution 4.0 International Public License.
https://react.dev/learn/manipulating-the-dom-with-refs