Работа с DOM с помощью Refs
React автоматически обновляет DOM для соответствия вашему результату рендеринга, поэтому вашим компонентам часто не нужно будет его изменять. Однако иногда вам может потребоваться доступ к элементам DOM, управляемым React — например, для фокусировки узла, прокрутки к нему или измерения его размера и положения. В React нет встроенного способа сделать это, поэтому вам понадобится ref к узлу DOM.
Вы узнаете
- Как получить доступ к узлу DOM, управляемому React, с помощью атрибута
ref - Как атрибут
refJSX связан с хукомuseRef - Как получить доступ к узлу DOM другого компонента
- В каких случаях безопасно изменять DOM, управляемый React
Получение ссылки на узел
Чтобы получить доступ к узлу DOM, управляемому React, сначала импортируйте хук useRef:
import { useRef } from 'react'; Затем используйте его для объявления ref внутри вашего компонента:
const myRef = useRef(null); Наконец, передайте свой ref в качестве атрибута 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 является наиболее распространённым случаем использования ref, хук useRef может использоваться для хранения других вещей вне React, например, идентификаторов таймеров. Подобно состоянию, ref сохраняется между рендерами. Refs — это переменные состояния, которые не вызывают перерендеринг при их установке. Подробнее о ref в Ссылка на значения с помощью Refs.
Пример: прокрутка к элементу
В компоненте может быть больше одного ref. В этом примере есть карусель из трёх изображений. Каждая кнопка центрирует изображение, вызывая браузерный метод 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> </> ); }
Глубокое погружение
Как управлять списком ref с помощью ref callback
В приведённых выше примерах количество ref заранее определено. Однако иногда вам может потребоваться ref для каждого элемента в списке, и вы не знаете, сколько их будет. Нечто подобное не сработает:
<ul>
{items.map((item) => {
// Doesn't work!
const ref = useRef(null);
return <li ref={ref} />;
})}
</ul>
Это происходит потому, что хуки должны вызываться только на верхнем уровне вашего компонента. Вы не можете вызвать useRef в цикле, в условии или внутри вызова map()
Один из возможных способов обойти это — получить единственный ref к родительскому элементу и затем использовать методы манипулирования DOM, такие как querySelectorAll, чтобы «найти» отдельные дочерние узлы из него. Однако это хрупко и может сломаться, если структура вашего DOM изменится.
Другое решение — передать функцию атрибуту ref. Это называется ref callback. React вызовет вашу ref callback с узлом DOM, когда придёт время установить ref, и с null пришло время его очистить. Это позволяет вам поддерживать свой собственный массив или Map и получать доступ к любому ref по его индексу или какому-либо идентификатору.
Этот пример показывает, как использовать этот подход для прокрутки к произвольному узлу в длинном списке:
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(); if (node) { map.set(cat, node); } else { 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. (Refs могут хранить любые значения!) ref callback на каждом элементе списка позаботится об обновлении Map:
<li
key={cat.id}
ref={node => {
const map = getMap();
if (node) {
// Add to the Map
map.set(cat, node);
} else {
// Remove from the Map
map.delete(cat);
}
}}
>
Это позволяет позже читать отдельные узлы DOM из Map.
Canary
Этот пример показывает другой подход для управления Map с помощью ref callback cleanup function.
<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 другого компонента
Когда вы устанавливаете ref на встроенный компонент, который выводит браузерный элемент, например, <input />, React установит свойство current этого ref в соответствующий узел DOM (например, фактический <input /> в браузере).
Однако, если вы попытаетесь установить ref на свой компонент, например, <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 других компонентов. Даже не к собственным дочерним! Это сделано намеренно. Refs — это аварийный выход, который следует использовать с осторожностью. Ручное изменение узлов DOM другого компонента делает ваш код ещё более хрупким.
Вместо этого компоненты, которые хотят экспонировать свои узлы DOM, должны выбрать это поведение. Компонент может указать, что он «передаёт» свой ref одному из своих дочерних элементов. Вот как 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> </> ); }
В системах дизайна распространённый паттерн — низкоуровневые компоненты, такие как кнопки, поля ввода и т. д., передают свои ref в узлы DOM. С другой стороны, высокоуровневые компоненты, такие как формы, списки или разделы страниц, обычно не раскрывают свои узлы 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 предоставить свой собственный специальный объект в качестве значения ref для родительского компонента. Таким образом, inputRef.current внутри компонента Form будет иметь только метод focus. В этом случае ref «handle» — это не узел DOM, а пользовательский объект, который вы создаёте внутри вызова useImperativeHandle
Когда React устанавливает ссылки
В React каждый обновление разделяется на два этапа:
- Во время рендеринга React вызывает ваши компоненты, чтобы выяснить, что должно отображаться на экране.
- Во время commit React применяет изменения к DOM.
Как правило, вы не хотите получать доступ к ref во время рендеринга. Это относится и к ref, хранящим узлы DOM. Во время первого рендеринга узлы DOM ещё не созданы, поэтому ref.current будет null. И во время рендеринга обновлений узлы DOM ещё не обновлены. Поэтому слишком рано их читать.
React устанавливает ref.current во время commit. Перед обновлением DOM React устанавливает соответствующие значения ref.current в null. После обновления DOM React немедленно устанавливает их в соответствующие узлы DOM.
Обычно вы обращаетесь к ref из обработчиков событий. Если вам нужно что-то сделать с ref, но нет конкретного события для этого, вам может потребоваться эффект. Мы рассмотрим эффекты на следующих страницах.
Глубокое погружение
Синхронное обновление состояния с помощью 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. Поэтому в момент, когда вы прокручиваете список до последнего элемента, задача ещё не добавлена. Вот почему прокрутка всегда отстаёт на один элемент.
Чтобы исправить эту проблему, можно заставить React синхронно обновить («сбросить») 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». Типичные примеры — управление фокусом, положением прокрутки или вызов браузерных 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 для <video>. Добавьте 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://18.react.dev/learn/manipulating-the-dom-with-refs