Ссылка на значения с помощью Refs¶
Когда вы хотите, чтобы компонент "запомнил" какую-то информацию, но не хотите, чтобы эта информация запускала новые рендеры, вы можете использовать ref.
Вы узнаете
- Как добавить ссылку в свой компонент
- Как обновить значение ссылки
- Чем отличаются ссылки от состояния
- Как безопасно использовать ссылки
Добавление ссылки в ваш компонент¶
Вы можете добавить ссылку в свой компонент, импортировав хук useRef
из React:
1 |
|
Внутри вашего компонента вызовите хук useRef
и в качестве единственного аргумента передайте начальное значение, на которое вы хотите сослаться. Например, вот ссылка на значение 0
:
1 |
|
useRef
возвращает объект, подобный этому:
1 2 3 |
|
Вы можете получить доступ к текущему значению этой ссылки через свойство ref.current
. Это значение намеренно мутабельно, то есть вы можете как читать, так и писать в него. Это как секретный карман вашего компонента, который React не отслеживает. (Именно это делает его "аварийным люком" из одностороннего потока данных React - подробнее об этом ниже!)
Здесь кнопка будет увеличивать ref.current
при каждом нажатии:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
Ссылка указывает на число, но, как и state, вы можете указать на что угодно: строку, объект или даже функцию. В отличие от state, ref - это обычный объект JavaScript со свойством current
, который можно читать и изменять.
Обратите внимание, что компонент не перерисовывается при каждом увеличении. Как и state, refs сохраняется React между перерисовками. Однако, установка состояния перерисовывает компонент. Изменение ссылки не делает этого!
Пример: создание секундомера¶
Вы можете комбинировать ссылки и состояние в одном компоненте. Например, давайте сделаем секундомер, который пользователь может запустить или остановить нажатием кнопки. Чтобы отобразить, сколько времени прошло с момента нажатия пользователем кнопки "Start", вам нужно будет отслеживать, когда была нажата кнопка Start и каково текущее время. Эта информация используется для рендеринга, поэтому вы будете хранить ее в состоянии:.
1 2 |
|
Когда пользователь нажмет кнопку "Start", вы будете использовать setInterval
, чтобы обновлять время каждые 10 миллисекунд:
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 |
|
Когда нажата кнопка "Stop", необходимо отменить существующий интервал, чтобы он перестал обновлять переменную состояния now
. Вы можете сделать это, вызвав clearInterval
, но вам нужно передать ему ID интервала, который ранее был возвращен вызовом setInterval
, когда пользователь нажал кнопку Start. Вам нужно где-то сохранить ID интервала. Поскольку ID интервала не используется для рендеринга, вы можете хранить его в ссылке:
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 |
|
Если часть информации используется для рендеринга, храните ее в состоянии. Если часть информации нужна только обработчикам событий и ее изменение не требует повторного рендеринга, использование ссылки может быть более эффективным.
Различия между refs и state¶
Возможно, вы думаете, что ссылки кажутся менее "строгими", чем state
- например, вы можете мутировать их вместо того, чтобы всегда использовать функцию установки состояния. Но в большинстве случаев вы захотите использовать state
. Рефы - это "аварийный люк", который вам не часто понадобится. Вот как сравниваются state
и refs
:
refs | state |
---|---|
useRef(initialValue) возвращает { current: initialValue } | useState(initialValue) возвращает текущее значение переменной состояния и функцию-установщик состояния ( [value, setValue] ) |
Не запускает повторный рендеринг при изменении. | Срабатывает повторный рендеринг при изменении. |
Mutable - вы можете изменять и обновлять значение current вне процесса рендеринга. | Immutable - вы должны использовать функцию установки состояния для изменения состояния, чтобы поставить в очередь на повторный рендеринг. |
Вы не должны читать (или записывать) значение current во время рендеринга. | Вы можете читать состояние в любое время. Однако каждый рендер имеет свой собственный snapshot состояния, которое не изменяется. |
Вот кнопка счетчика, которая реализована с использованием состояния:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
Поскольку значение count
отображается, имеет смысл использовать для него значение состояния. Когда значение счетчика устанавливается с помощью setCount()
, React перерисовывает компонент, и экран обновляется, чтобы отразить новый счетчик.
Если бы вы попытались реализовать это с помощью ссылки, React никогда бы не перерисовал компонент, и вы бы никогда не увидели изменения счетчика! Посмотрите, как нажатие на эту кнопку не обновляет ее текст:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
Вот почему чтение ref.current
во время рендеринга приводит к ненадежному коду. Если вам это нужно, используйте вместо этого state.
Как useRef работает внутри?
Хотя и useState
и useRef
предоставляются React, в принципе useRef
может быть реализован поверх useState
. Вы можете представить, что внутри React useRef
реализован следующим образом:
1 2 3 4 5 6 7 |
|
Во время первого рендеринга useRef
возвращает { current: initialValue }
. Этот объект хранится в памяти React, поэтому при следующем рендере будет возвращен тот же объект. Обратите внимание, что в этом примере сеттер состояния не используется. Он не нужен, потому что useRef
всегда должен возвращать один и тот же объект!
React предоставляет встроенную версию useRef
, потому что это достаточно часто встречается на практике. Но вы можете думать о ней как об обычной переменной состояния без сеттера. Если вы знакомы с объектно-ориентированным программированием, refs может напомнить вам поля экземпляра, но вместо this.something
вы напишите somethingRef.current
.
Когда использовать refs¶
Как правило, вы используете refs, когда вашему компоненту нужно "выйти за пределы" React и взаимодействовать с внешними API - часто с API браузера, которые не влияют на внешний вид компонента. Вот несколько таких редких ситуаций:
- Хранение идентификаторов таймаута
- Хранение и манипулирование элементами DOM, о чем мы расскажем на следующей странице
- Хранение других объектов, которые не нужны для вычисления JSX.
Если вашему компоненту нужно хранить какое-то значение, но оно не влияет на логику рендеринга, выбирайте refs.
Лучшие практики для refs¶
Следование этим принципам сделает ваши компоненты более предсказуемыми:
- Рассматривайте ссылки как аварийный люк. Ссылки полезны, когда вы работаете с внешними системами или API браузера. Если большая часть логики вашего приложения и потока данных зависит от ссылок, возможно, вам стоит пересмотреть свой подход.
- Не читайте и не записывайте
ref.current
во время рендеринга. Если какая-то информация необходима во время рендеринга, используйте state вместо этого. Поскольку React не знает, когда изменяетсяref.current
, даже чтение его во время рендеринга делает поведение вашего компонента труднопредсказуемым. (Единственным исключением из этого является код типаif (!ref.current) ref.current = new Thing()
, который устанавливает ссылку только один раз во время первого рендеринга).
Ограничения React state не распространяются на refs. Например, состояние действует как снимок для каждого рендера и не обновляется синхронно Но когда вы изменяете текущее значение ссылки, оно немедленно меняется:
1 2 |
|
Это происходит потому, что ссылка сама по себе является обычным объектом JavaScript, и поэтому ведет себя как обычный объект.
Вам также не нужно беспокоиться о избегании мутации, когда вы работаете с ref. До тех пор, пока объект, который вы мутируете, не используется для рендеринга, React не волнует, что вы делаете с ref или его содержимым.
Ссылки и DOM¶
Вы можете указать ссылку на любое значение. Однако наиболее часто ссылка используется для доступа к элементу DOM. Например, это удобно, если вы хотите программно сфокусировать вход. Когда вы передаете ссылку в атрибут ref
в JSX, например <div ref={myRef}>
, React помещает соответствующий элемент DOM в myRef.current
. Подробнее об этом вы можете прочитать в Manipulating the DOM with Refs..
Итоги
- Ссылки - это люк для хранения значений, которые не используются для рендеринга. Они нечасто вам понадобятся.
- Ссылка - это обычный объект JavaScript с единственным свойством
current
, которое вы можете прочитать или установить. - Вы можете попросить React дать вам ссылку, вызвав хук
useRef
. - Как и состояние, ссылки позволяют сохранять информацию между повторными рендерингами компонента.
- В отличие от состояния, установка
текущего
значения ссылки не вызывает повторного рендеринга. - Не читайте и не записывайте
ref.current
во время рендеринга. Это сделает ваш компонент трудно предсказуемым.
Задачи¶
1. Исправление неработающего входа в чат¶
Введите сообщение и нажмите "Отправить". Вы заметите, что перед появлением сообщения "Отправлено!" произойдет трехсекундная задержка. Во время этой задержки вы увидите кнопку "Отменить". Нажмите на нее. Эта кнопка "Отменить" должна остановить появление сообщения "Отправлено!". Она делает это, вызывая clearTimeout
для идентификатора таймаута, сохраненного во время handleSend
. Однако даже после нажатия кнопки "Undo" сообщение "Sent!" все равно появляется. Найдите причину неработоспособности и устраните ее.
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 |
|
Показать подсказку
Обычные переменные, такие как let timeoutID
, не "выживают" между повторными рендерами, потому что каждый рендер запускает ваш компонент (и инициализирует его переменные) с нуля. Должны ли вы хранить идентификатор таймаута в другом месте?
Показать решение
Всякий раз, когда ваш компонент перестраивается (например, при установке состояния), все локальные переменные инициализируются с нуля. Вот почему вы не можете сохранить идентификатор таймаута в локальной переменной типа timeoutID
, а затем ожидать, что другой обработчик событий "увидит" его в будущем. Вместо этого сохраните его в ссылке, которую React будет сохранять между рендерами.
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 |
|
2. Исправьте компонент, который не смог перерисоваться¶
Эта кнопка должна переключаться между отображением "Вкл" и "Выкл". Однако она всегда показывает "Выключено". Что не так с этим кодом? Исправьте это.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
Показать решение
В этом примере текущее значение ссылки используется для расчета вывода рендеринга: {isOnRef.current ? 'On' : 'Off'}
. Это признак того, что данная информация не должна быть в ссылке, а должна быть помещена в состояние. Чтобы исправить это, удалите ссылку и используйте вместо нее state:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
3. Исправление debounce¶
В этом примере все обработчики нажатия на кнопку "debounced". Чтобы увидеть, что это значит, нажмите на одну из кнопок. Обратите внимание, что через секунду появится сообщение. Если вы нажмете кнопку в ожидании сообщения, таймер сбросится. Таким образом, если вы будете нажимать одну и ту же кнопку много раз, сообщение появится только через секунду после того, как вы перестанете нажимать. Дебаунсинг позволяет отложить выполнение какого-либо действия до тех пор, пока пользователь не "перестанет делать что-то".
Этот пример работает, но не совсем так, как было задумано. Кнопки не являются независимыми. Чтобы увидеть проблему, нажмите на одну из кнопок, а затем сразу же нажмите на другую. Можно было бы ожидать, что после задержки вы увидите сообщения обеих кнопок. Но отображается только сообщение последней кнопки. Сообщение первой кнопки теряется.
Почему кнопки мешают друг другу? Найдите и устраните проблему.
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 |
|
Показать подсказку
Переменная ID последнего таймаута является общей для всех компонентов DebouncedButton
. Вот почему нажатие на одну кнопку сбрасывает таймаут другой кнопки. Можете ли вы хранить отдельный ID таймаута для каждой кнопки?
Показать решение
Такая переменная, как timeoutID
, является общей для всех компонентов. Вот почему нажатие на вторую кнопку сбрасывает таймаут первой кнопки. Чтобы исправить это, вы можете хранить таймаут в ссылке. Каждая кнопка получит свой собственный 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 |
|
4. Прочитать последнее состояние¶
В этом примере после нажатия кнопки "Отправить" происходит небольшая задержка перед отображением сообщения. Введите "hello", нажмите "Отправить", а затем быстро отредактируйте ввод снова. Несмотря на ваши правки, оповещение все равно покажет "hello" (это было значение state на момент нажатия кнопки).
Обычно такое поведение - это то, что вы хотите видеть в приложении. Однако иногда могут возникнуть ситуации, когда вы хотите, чтобы асинхронный код считывал последнюю версию некоторого состояния. Можете ли вы придумать, как сделать так, чтобы оповещение показывало текущий текст ввода, а не тот, что был в момент нажатия?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
|
Показать решение
Состояние работает как моментальный снимок, поэтому вы не можете прочитать последнее состояние из асинхронной операции, такой как таймаут. Однако вы можете сохранить последний введенный текст в ссылке. Ссылка является изменяемой, поэтому вы можете прочитать свойство current
в любое время. Поскольку текущий текст также используется для рендеринга, в этом примере вам понадобится и переменная состояния (для рендеринга), и ссылка (чтобы прочитать ее в таймауте). Вам нужно будет обновить текущее значение 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 |
|
Источник — https://react.dev/learn/referencing-values-with-refs