Примечание: Эта статья предполагает базовое понимание того, что такое рефлексы в React.
Несмотря на то, что refs — это изменяемые контейнеры, в которых теоретически можно хранить произвольные значения, чаще всего они используются для получения доступа к узлу DOM:
const ref = React.useRef(null)
return <input ref={ref} defaultValue="Hello world" />
ref
— это зарезервированное свойство встроенных примитивов, в котором React будет хранить узел DOM после его рендеринга. Он будет установлен обратно в null, когда компонент будет размонтирован.
Взаимодействие с рефлексами
Для большинства взаимодействий вам не нужно обращаться к базовому узлу DOM, поскольку React будет автоматически обрабатывать обновления. Хорошим примером, когда вам может понадобиться ссылка, является управление фокусом.
Есть хороший RFC от Девона Говетта, который предлагает добавить FocusManagement в react-dom, но прямо сейчас в React нет ничего, что помогло бы нам с этим.
Фокус с эффектом
Итак, как бы вы сейчас сфокусировали элемент ввода после его рендеринга? (Я знаю, что автофокус существует, это пример. Если вас это беспокоит, представьте, что вы хотите анимировать узел).
Большинство кода, который я видел, пытается это сделать:
const ref = React.useRef(null)
React.useEffect(() => {
ref.current?.focus()
}, [])
return <input ref={ref} defaultValue="Hello world" />
Это в основном нормально и не нарушает никаких правил. Пустой массив зависимостей — это нормально, потому что единственное, что используется внутри — это ссылка, которая стабильна. Линтер не будет жаловаться на добавление его в массив зависимостей, и ссылка также не считывается во время рендеринга (что может быть проблематично с одновременными функциями react).
Эффект будет запущен один раз «при монтировании» (дважды в строгом режиме). К этому времени React уже заполнит ref узлом DOM, и мы сможем сфокусировать его.
Тем не менее, это не самый лучший способ, и в некоторых более сложных ситуациях он имеет некоторые оговорки.
В частности, предполагается, что ссылка «заполнена» в момент запуска эффекта. Если этого нет, например, потому что вы передаете ссылку пользовательскому компоненту, который откладывает рендеринг или показывает ввод только после какого-то другого взаимодействия с пользователем, содержимое ссылки будет нулевым в момент запуска эффекта и ничего не будет сфокусировано:
function App() {
const ref = React.useRef(null)
React.useEffect(() => {
// 🚨 ref.current is always null when this runs
ref.current?.focus()
}, [])
return <Form ref={ref} />
}
const Form = React.forwardRef((props, ref) => {
const [show, setShow] = React.useState(false)
return (
<form>
<button type="button" onClick={() => setShow(true)}>
show
</button>
// 🧐 ref is attached to the input, but it's conditionally rendered
// so it won't be filled when the above effect runs
{show && <input ref={ref} />}
</form>
)
})
Вот что происходит:
- Форма рендерится.
- input не отображается, ссылка по-прежнему нулевая.
- Запускается эффект, который ничего не делает.
- input отображается, ссылка заполняется, но не фокусируется, потому что эффект больше не запускается.
Проблема в том, что эффект «привязан» к функции рендеринга формы, в то время как на самом деле мы хотим выразить: «Фокусировать вход, когда вход отрисовывается», а не «когда форма монтируется».
Рефлексы обратного вызова
Именно здесь в игру вступают обратные вызовы. Если вы когда-нибудь смотрели на объявления типов для рефлексов, мы увидим, что мы можем передать в него не только объект ref, но и функцию:
type Ref<T> = RefCallback<T> | RefObject<T> | null
Концептуально, мне нравится думать о рефссылках на элементах react как о функциях, которые вызываются после рендеринга компонента. Эта функция получает отрисованный узел DOM, переданный в качестве аргумента. Если элемент react размонтируется, она будет вызвана еще раз с null.
Поэтому передача ссылки из useRef (объекта RefObject) в элемент react — это просто синтаксический сахар для:
<input
ref={(node) => {
ref.current = node;
}}
defaultValue="Hello world"
/>
Позвольте мне подчеркнуть это еще раз:
Все реквизиты ref — это просто функции!
И эти функции выполняются после рендеринга, где совершенно нормально выполнять побочные эффекты. Возможно, было бы лучше, если бы ref просто вызывался onAfterRender или что-то в этом роде.
Зная это, что мешает нам сфокусировать ввод прямо внутри обратного вызова ref, где у нас есть прямой доступ к узлу?
<input
ref={(node) => {
node?.focus()
}}
defaultValue="Hello world"
/>
Ну, есть одна маленькая деталь: React будет запускать эту функцию после каждого рендера. Поэтому, если мы не против фокусировать наш ввод так часто (что, скорее всего, не так), мы должны сказать React, чтобы он запускал эту функцию только тогда, когда мы этого хотим.
UseCallback на помощь
К счастью, React использует ссылочную стабильность, чтобы проверить, нужно ли запускать реферер обратного вызова или нет. Это означает, что если мы передадим ему один и тот же рефлекс, выполнение будет пропущено.
И именно здесь вступает в дело useCallback, потому что именно так мы гарантируем, что функция не будет создана без необходимости. Может быть, поэтому они и называются callback-refs — потому что вы должны постоянно оборачивать их в useCallback. 😂
Вот окончательное решение:
const ref = React.useCallback((node) => {
node?.focus()
}, [])
return <input ref={ref} defaultValue="Hello world" />
Если сравнивать его с первоначальным вариантом, то здесь меньше кода и используется только один хук вместо двух. Кроме того, он будет работать во всех ситуациях, потому что обратный вызов привязан к жизненному циклу узла dom, а не компонента, который его монтирует. Кроме того, он не будет выполняться дважды в строгом режиме (при запуске в среде разработки), что кажется важным для многих.
И как показано в этой скрытой жемчужине в (старой) документации по react, вы можете использовать его для запуска любых побочных эффектов, например, вызвать в нем setState. Я просто оставлю пример здесь, потому что он на самом деле довольно хорош:
function MeasureExample() {
const [height, setHeight] = React.useState(0)
const measuredRef = React.useCallback(node => {
if (node !== null) {
setHeight(node.getBoundingClientRect().height)
}
}, [])
return (
<>
<h1 ref={measuredRef}>Hello, world</h1>
<h2>The above header is {Math.round(height)}px tall</h2>
</>
)
}
Поэтому, пожалуйста, если вам нужно взаимодействовать с узлами DOM непосредственно после их рендеринга, постарайтесь не переходить к useRef + useEffect напрямую, а рассмотреть возможность использования обратных ссылок вместо этого.
На сегодня это все. Не стесняйтесь обращаться ко мне в twitter
если у вас есть вопросы, или просто оставьте комментарий ниже. ⬇️