Манипулирование DOM с помощью Refs¶
React автоматически обновляет DOM в соответствии с вашим выводом на экран, поэтому вашим компонентам не часто требуется манипулировать им. Однако иногда вам может понадобиться доступ к элементам DOM, управляемым React - например, для фокусировки узла, прокрутки к нему или измерения его размера и положения. Встроенного способа сделать это в React нет, поэтому вам понадобится ref на узел DOM.
- Как получить доступ к узлу DOM, управляемому React, с помощью атрибута
ref
- Как JSX-атрибут
ref
связан с хукомuseRef
. - Как получить доступ к DOM-узлу другого компонента
- В каких случаях безопасно изменять DOM под управлением React
Получение ссылки на узел {/getting-a-ref-to-the-node/}¶
Чтобы получить доступ к узлу DOM, управляемому React, сначала импортируйте хук useRef
:
1 |
|
Затем используйте его для объявления ссылки внутри вашего компонента:
1 |
|
Наконец, передайте его узлу DOM в качестве атрибута ref
:
1 |
|
The useRef
Hook returns an object with a single property called current
. Initially, myRef.current
will be null
. When React creates a DOM node for this <div>
, React will put a reference to this node into myRef.current
. You can then access this DOM node from your event handlers and use the built-in browser APIs defined on it.
1 2 |
|
Пример: Фокусировка текстового ввода {/example-focusing-a-text-input/}¶
В этом примере нажатие на кнопку фокусирует ввод:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
To implement this:
- Declare
inputRef
with theuseRef
Hook. - Pass it as
<input ref={inputRef}>
. This tells React to put this<input>
’s DOM node intoinputRef.current
. - In the
handleClick
function, read the input DOM node frominputRef.current
and callfocus()
on it withinputRef.current.focus()
. - Pass the
handleClick
event handler to<button>
withonClick
.
While DOM manipulation is the most common use case for refs, the useRef
Hook can be used for storing other things outside React, like timer IDs. Similarly to state, refs remain between renders. Refs are like state variables that don’t trigger re-renders when you set them. Read about refs in Referencing Values with Refs.
Example: Scrolling to an element {/example-scrolling-to-an-element/}¶
You can have more than a single ref in a component. In this example, there is a carousel of three images. Each button centers an image by calling the browser scrollIntoView()
method on the corresponding DOM node:
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 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
|
Как управлять списком ссылок с помощью обратного вызова {/how-to-manage-a-list-of-refs-using-a-ref-callback/}¶
В приведенных выше примерах существует предопределенное количество ссылок. Однако иногда вам может понадобиться ссылка на каждый элемент списка, и вы не знаете, сколько их будет. Что-то вроде этого не будет работать:
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 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
|
В этом примере itemsRef
не содержит ни одного узла DOM. Вместо этого он содержит Map от ID элемента к узлу DOM. (Ссылки могут содержать любые значения!) Обратный вызов ref
для каждого элемента списка заботится об обновлении карты:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
This lets you read individual DOM nodes from the Map later.
Accessing another component’s DOM nodes {/accessing-another-components-dom-nodes/}¶
When you put a ref on a built-in component that outputs a browser element like <input />
, React will set that ref’s current
property to the corresponding DOM node (such as the actual <input />
in the browser).
However, if you try to put a ref on your own component, like <MyInput />
, by default you will get null
. Here is an example demonstrating it. Notice how clicking the button does not focus the input:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
|
Чтобы помочь вам заметить проблему, React также выводит ошибку в консоль:
Предупреждение: Компонентам функции нельзя давать ссылки. Попытки получить доступ к этой ссылке будут безуспешными. Вы хотели использовать React.forwardRef()?
Это происходит потому, что по умолчанию React не позволяет компоненту обращаться к узлам DOM других компонентов. Даже своим собственным детям! Это намеренно. Ссылки - это аварийный люк, который следует использовать очень редко. Ручное манипулирование DOM-узлами другого компонента делает ваш код еще более хрупким.
Вместо этого, компоненты, которые хотят раскрыть свои узлы DOM, должны оптировать такое поведение. Компонент может указать, что он "переадресует" свою ссылку одному из своих дочерних компонентов. Вот как MyInput
может использовать API forwardRef
:
1 2 3 |
|
This is how it works:
<MyInput ref={inputRef} />
tells React to put the corresponding DOM node intoinputRef.current
. However, it’s up to theMyInput
component to opt into that–by default, it doesn’t.- The
MyInput
component is declared usingforwardRef
. This opts it into receiving theinputRef
from above as the secondref
argument which is declared afterprops
. MyInput
itself passes theref
it received to the<input>
inside of it.
Now clicking the button to focus the input works:
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 с помощью императивного дескриптора {/exposing-a-subset-of-the-api-with-an-imperative-handle/}¶
В приведенном выше примере 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 присоединяет ссылки {/when-react-attaches-the-refs/}¶
В React каждое обновление делится на две фазы:
- Во время render, React вызывает ваши компоненты, чтобы выяснить, что должно быть на экране.
- Во время commit, React применяет изменения в DOM.
В общем, вы не хотите обращаться к рефкам во время рендеринга. Это относится и к ссылкам, содержащим узлы DOM. Во время первого рендеринга узлы DOM еще не были созданы, поэтому ref.current
будет null
. А во время рендеринга обновлений, узлы DOM еще не были обновлены. Поэтому читать их еще рано.
React устанавливает ref.current
во время фиксации. Перед обновлением DOM, React устанавливает затронутые значения ref.current
в null
. После обновления DOM, React немедленно устанавливает их в соответствующие узлы DOM.
Обычно вы обращаетесь к рефкам из обработчиков событий. Если вы хотите что-то сделать с рефкой, но нет конкретного события, в котором это можно сделать, вам может понадобиться эффект. Мы обсудим эффекты на следующих страницах.
Промывка обновлений состояния синхронно с помощью flushSync {/flush-state-updates-synchronously-with-flush-sync/}¶
Рассмотрим код, подобный этому, который добавляет новый 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 с помощью ссылок {/best-practices-for-dom-manipulation-with-refs/}¶
Ссылки - это аварийный люк. Вы должны использовать их только тогда, когда вам нужно "выйти за пределы React". Обычные примеры этого - управление фокусом, позицией прокрутки или вызов API браузера, которые React не раскрывает.
Если вы придерживаетесь неразрушающих действий, таких как фокусировка и прокрутка, вы не должны столкнуться с какими-либо проблемами. Однако, если вы попытаетесь изменить DOM вручную, вы рискуете вступить в конфликт с изменениями, которые вносит React.
Чтобы проиллюстрировать эту проблему, данный пример включает в себя приветственное сообщение и две кнопки. Первая кнопка переключает свое присутствие, используя conditional rendering и state, как вы обычно делаете в 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 |
|
1 2 3 4 5 |
|
After you’ve manually removed the DOM element, trying to use setState
to show it again will lead to a crash. This is because you’ve changed the DOM, and React doesn’t know how to continue managing it correctly.
Avoid changing DOM nodes managed by React. Modifying, adding children to, or removing children from elements that are managed by React can lead to inconsistent visual results or crashes like above.
However, this doesn’t mean that you can’t do it at all. It requires caution. You can safely modify parts of the DOM that React has no reason to update. For example, if some <div>
is always empty in the JSX, React won’t have a reason to touch its children list. Therefore, it is safe to manually add or remove elements there.
\<Recap>
- Refs are a generic concept, but most often you’ll use them to hold DOM elements.
- You instruct React to put a DOM node into
myRef.current
by passing<div ref={myRef}>
. - Usually, you will use refs for non-destructive actions like focusing, scrolling, or measuring DOM elements.
- A component doesn’t expose its DOM nodes by default. You can opt into exposing a DOM node by using
forwardRef
and passing the secondref
argument down to a specific node. - Avoid changing DOM nodes managed by React.
- Если вы изменяете узлы DOM, управляемые React, изменяйте те части, которые React не имеет причин обновлять.
\</Recap>
\<Проблемы>
Воспроизведение и пауза видео {/play-and-pause-the-video/}¶
В этом примере кнопка переключает переменную состояния для перехода между воспроизведением и паузой. Однако для того, чтобы действительно воспроизвести или поставить видео на паузу, переключения состояния недостаточно. Вам также необходимо вызвать 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 |
|
1 2 3 4 |
|
Для решения дополнительной задачи синхронизируйте кнопку "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 |
|
1 2 3 4 |
|
Для работы со встроенными элементами управления браузера вы можете добавить обработчики onPlay
и onPause
к элементу <video>
и вызвать из них setIsPlaying
. Таким образом, если пользователь воспроизводит видео с помощью элементов управления браузера, состояние будет соответствующим образом изменено.
\</Solution>
Фокусировка поля поиска {/focus-the-search-field/}¶
Сделайте так, чтобы нажатие на кнопку "Поиск" наводило фокус на поле.
1 2 3 4 5 6 7 8 9 10 |
|
1 2 3 4 |
|
\<Решение>
Добавьте ссылку на вход и вызовите focus()
на узле DOM, чтобы сфокусировать его:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
|
1 2 3 4 |
|
\</Solution>
Прокрутка карусели изображений {/scrolling-an-image-carousel/}¶
Эта карусель изображений имеет кнопку "Next", которая переключает активное изображение. Заставьте галерею прокручиваться горизонтально до активного изображения по щелчку. Для этого нужно вызвать scrollIntoView()
на DOM-узле активного изображения:
1 2 3 4 5 |
|
\< Подсказка>
Для этого упражнения не обязательно иметь ссылку на каждое изображение. Достаточно иметь ссылку на текущее активное изображение или на сам список. Используйте flushSync
для обеспечения обновления DOM до прокрутки.
\</Hint>
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 |
|
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 |
|
\<Решение>
Вы можете объявить 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 |
|
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 |
|
\</Solution>
Фокусировка поля поиска с помощью отдельных компонентов {/focus-the-search-field-with-separate-components/}¶
Сделайте так, чтобы нажатие на кнопку "Поиск" наводило фокус на поле. Обратите внимание, что каждый компонент определен в отдельном файле и не должен быть перемещен из него. Как соединить их вместе?
\<Hint>
Вам понадобится forwardRef
, чтобы открыть узел DOM из вашего собственного компонента, такого как SearchInput
.
\</Hint>
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
1 2 3 |
|
1 2 3 |
|
1 2 3 4 |
|
\<Solution>
You’ll need to add an onClick
prop to the SearchButton
, and make the SearchButton
pass it down to the browser <button>
. You’ll also pass a ref down to <SearchInput>
, which will forward it to the real <input>
and populate it. Finally, in the click handler, you’ll call focus
on the DOM node stored inside that ref.
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 |
|
1 2 3 4 |
|
\</Solution>
\</Challenges>