Spec-Zone.ru › React 18

Обращение к значениям с помощью Refs

Когда вы хотите, чтобы компонент «запомнил» некоторую информацию, но не хотите, чтобы эта информация вызывала новые рендеринги, вы можете использовать ref.

Вы узнаете

  • Как добавить ref в ваш компонент
  • Как обновить значение ref
  • В чём различие между ref и состоянием
  • Как безопасно использовать ref

Добавление ref в ваш компонент

Вы можете добавить ref в ваш компонент, импортировав useRef Hook из React:

import { useRef } from 'react';

Внутри вашего компонента вызовите useRef Hook и передайте начальное значение, которое вы хотите использовать, в качестве единственного аргумента. Например, вот ref к значению 0:

const ref = useRef(0);

useRef возвращает объект такого вида:

{ 
  current: 0 // The value you passed to useRef
}
An arrow with 'current' written on it stuffed into a pocket with 'ref' written on it.

Иллюстрация предоставлена Рэйчел Ли Наборс

Вы можете получить текущее значение этого ref через свойство ref.current. Это значение преднамеренно изменяемое, что означает, что вы можете как читать, так и записывать в него. Это как секретный карман вашего компонента, который React не отслеживает. (Это то, что делает его «выходным» из одностороннего потока данных React — подробнее об этом ниже!)

Вот как кнопка будет увеличивать ref.current при каждом нажатии:

import { useRef } from 'react';

export default function Counter() {
  let ref = useRef(0);

  function handleClick() {
    ref.current = ref.current + 1;
    alert('You clicked ' + ref.current + ' times!');
  }

  return (
    <button onClick={handleClick}>
      Click me!
    </button>
  );
}

ref указывает на число, но, как и состояние, вы можете указывать на что угодно: строку, объект или даже функцию. В отличие от состояния, ref — это обычный JavaScript-объект со свойством current, которое вы можете читать и изменять.

Обратите внимание, что компонент не перерендерится при каждом увеличении. Как и состояние, refs сохраняются React между перерендерами. Однако установка состояния перерендерит компонент. Изменение ref этого не делает!

Пример: создание секундомера

Вы можете комбинировать ref и состояние в одном компоненте. Например, давайте создадим секундомер, который пользователь может запускать или останавливать, нажимая кнопку. Для отображения прошедшего времени с момента нажатия пользователем «Старт» вам необходимо отслеживать время нажатия кнопки «Старт» и текущее время. Эта информация используется для рендеринга, поэтому вы сохраните её в состоянии:

const [startTime, setStartTime] = useState(null);
const [now, setNow] = useState(null);

Когда пользователь нажимает «Старт», вы используете setInterval для обновления времени каждые 10 миллисекунд:

import { useState } from 'react';

export default function Stopwatch() {
  const [startTime, setStartTime] = useState(null);
  const [now, setNow] = useState(null);

  function handleStart() {
    // Start counting.
    setStartTime(Date.now());
    setNow(Date.now());

    setInterval(() => {
      // Update the current time every 10ms.
      setNow(Date.now());
    }, 10);
  }

  let secondsPassed = 0;
  if (startTime != null && now != null) {
    secondsPassed = (now - startTime) / 1000;
  }

  return (
    <>
      <h1>Time passed: {secondsPassed.toFixed(3)}</h1>
      <button onClick={handleStart}>
        Start
      </button>
    </>
  );
}

Когда нажимается кнопка «Стоп», необходимо отменить существующий интервал, чтобы он перестал обновлять состояние now переменной. Это можно сделать, вызвав clearInterval, но вам нужно указать идентификатор интервала, который ранее был возвращён вызовом setInterval при нажатии пользователем «Старт». Вам нужно где-то сохранить идентификатор интервала. Поскольку идентификатор интервала не используется для рендеринга, вы можете сохранить его в ref:

import { useState, useRef } from 'react';

export default function Stopwatch() {
  const [startTime, setStartTime] = useState(null);
  const [now, setNow] = useState(null);
  const intervalRef = useRef(null);

  function handleStart() {
    setStartTime(Date.now());
    setNow(Date.now());

    clearInterval(intervalRef.current);
    intervalRef.current = setInterval(() => {
      setNow(Date.now());
    }, 10);
  }

  function handleStop() {
    clearInterval(intervalRef.current);
  }

  let secondsPassed = 0;
  if (startTime != null && now != null) {
    secondsPassed = (now - startTime) / 1000;
  }

  return (
    <>
      <h1>Time passed: {secondsPassed.toFixed(3)}</h1>
      <button onClick={handleStart}>
        Start
      </button>
      <button onClick={handleStop}>
        Stop
      </button>
    </>
  );
}

Если какой-либо фрагмент информации используется для рендеринга, сохраните его в состоянии. Если какой-либо фрагмент информации необходим только обработчикам событий, и его изменение не требует перерендеринга, использование ref может быть более эффективным.

Различия между ref и состоянием

Возможно, вы думаете, что ref менее «строгие», чем состояние — вы можете изменять их вместо того, чтобы всегда использовать функцию установки состояния. Но в большинстве случаев вы захотите использовать состояние. Refs — это «выходной» механизм, который вам не понадобится часто. Вот сравнение состояния и refs:

refs состояние
useRef(initialValue) возвращает { current: initialValue } useState(initialValue) возвращает текущее значение переменной состояния и функцию установки состояния ([value, setValue])
Не вызывает перерендеринг при изменении. Вызывает перерендеринг при изменении.
Изменяемое — вы можете изменять и обновлять значение current вне процесса рендеринга. «Неизменяемое» — вы должны использовать функцию установки состояния для изменения переменных состояния, чтобы вызвать перерендеринг.
Не следует читать (или записывать) значение current во время рендеринга. Вы можете читать состояние в любое время. Однако каждый рендеринг имеет свой снэпшот состояния, который не изменяется.

Вот кнопка счётчика, реализованная с состоянием:

import { useState } from 'react';

export default function Counter() {
  const [count, setCount] = useState(0);

  function handleClick() {
    setCount(count + 1);
  }

  return (
    <button onClick={handleClick}>
      You clicked {count} times
    </button>
  );
}

Поскольку значение count отображается, имеет смысл использовать значение состояния для него. Когда значение счётчика устанавливается с помощью setCount(), React перерендерит компонент, и экран обновляется, чтобы отразить новое значение.

Если вы попытаетесь реализовать это с помощью ref, React никогда не перерендерит компонент, поэтому вы никогда не увидите изменения счётчика! Обратите внимание, что нажатие этой кнопки не обновляет текст:

import { useRef } from 'react';

export default function Counter() {
  let countRef = useRef(0);

  function handleClick() {
    // This doesn't re-render the component!
    countRef.current = countRef.current + 1;
  }

  return (
    <button onClick={handleClick}>
      You clicked {countRef.current} times
    </button>
  );
}

Вот почему чтение ref.current во время рендеринга приводит к ненадежному коду. Если вам это нужно, используйте состояние вместо этого.

Подробный разбор

Как работает useRef внутри?

Хотя как useState так и useRef предоставляются React, в принципе useRef можно реализовать на основе useState. Вы можете представить, что внутри React useRef реализовано так:

// Inside of React
function useRef(initialValue) {
  const [ref, unused] = useState({ current: initialValue });
  return ref;
}

Во время первого рендеринга useRef возвращает { current: initialValue }. Этот объект хранится React, поэтому при следующем рендеринге будет возвращён тот же объект. Обратите внимание, как функция установки состояния в этом примере не используется. Она не нужна, потому что useRef всегда должен возвращать один и тот же объект!

React предоставляет встроенную версию useRef, потому что она достаточно распространена на практике. Но вы можете рассматривать её как обычную переменную состояния без функции установки. Если вы знакомы с объектно-ориентированным программированием, refs могут напомнить вам о полях экземпляра — но вместо this.something вы пишете somethingRef.current.

Когда использовать refs

Как правило, вы будете использовать ref, когда вашему компоненту необходимо «выйти за пределы» React и взаимодействовать с внешними API — часто с API браузера, которые не повлияют на отображение компонента. Вот несколько таких редких случаев:

  • Хранение идентификаторов таймаутов
  • Хранение и управление DOM-элементами, о чём мы поговорим на следующей странице
  • Хранение других объектов, которые не нужны для расчёта JSX.

Если вашему компоненту нужно хранить некоторое значение, но оно не влияет на логику рендеринга, выберите ref.

Рекомендации по использованию refs

Следование этим принципам сделает ваши компоненты более предсказуемыми:

  • Рассматривайте ref как выходной механизм. Refs полезны, когда вы работаете с внешними системами или API браузера. Если большая часть логики и потока данных вашего приложения полагается на refs, возможно, стоит пересмотреть ваш подход.
  • Не читайте и не записывайте значение ref.current во время рендеринга. Если какая-то информация нужна во время рендеринга, используйте состояние вместо этого. Так как React не знает, когда изменяется значение ref.current, даже чтение его во время рендеринга делает поведение вашего компонента сложным для предсказания. (Единственное исключение — код вроде if (!ref.current) ref.current = new Thing(), который устанавливает ref только один раз во время первого рендеринга.)

Ограничения состояния React не применяются к refs. Например, состояние действует как снимок для каждого рендеринга и не обновляется синхронно. Но когда вы изменяете текущее значение ref, оно изменяется немедленно:

ref.current = 5;
console.log(ref.current); // 5

Это потому, что сам ref — это обычный JavaScript-объект, и поэтому он ведёт себя как таковой.

Также вам не нужно беспокоиться об избегании мутации, когда работаете с ref. До тех пор, пока объект, который вы изменяете, не используется для рендеринга, React не заботится о том, что вы делаете с ref или его содержимым.

Refs и DOM

Вы можете указать ref на любое значение. Однако наиболее распространённым случаем использования ref является доступ к DOM-элементу. Например, это удобно, если вы хотите программно сфокусировать поле ввода. Когда вы передаёте ref в атрибут ref в JSX, как <div ref={myRef}>, React поместит соответствующий DOM-элемент в myRef.current. Как только элемент будет удален из DOM, React обновит myRef.current на null. Подробнее об этом можно прочитать в Управлении DOM с помощью Refs.

Резюме

  • Refs — это выходной механизм для хранения значений, которые не используются для рендеринга. Вам они не понадобятся часто.
  • Ref — это обычный JavaScript-объект с одним свойством под названием current, которое вы можете читать или устанавливать.
  • Вы можете попросить React предоставить вам ref, вызвав useRef Hook.
  • Как и состояние, refs позволяют сохранять информацию между перерендерами компонента.
  • В отличие от состояния, установка значения current ref не вызывает перерендеринг.
  • Не читайте и не записывайте ref.current во время рендеринга. Это делает ваш компонент сложным для предсказания.

Попробуйте выполнить некоторые задания

Задание 1 из 4:
Исправить неработающий поле ввода чата

Введите сообщение и нажмите «Отправить». Вы заметите, что появляется трёхсекундная задержка, прежде чем вы увидите всплывающее окно «Отправлено!». Во время этой задержки вы можете увидеть кнопку «Отменить». Нажмите её. Эта кнопка «Отменить» должна предотвратить появление сообщения «Отправлено!», сделав это, вызвав clearTimeout для идентификатора таймаута, сохранённого во время handleSend. Однако даже после нажатия «Отменить» сообщение «Отправлено!» всё равно появляется. Найдите причину, по которой это не работает, и исправьте её.

import { useState } from 'react';

export default function Chat() {
  const [text, setText] = useState('');
  const [isSending, setIsSending] = useState(false);
  let timeoutID = null;

  function handleSend() {
    setIsSending(true);
    timeoutID = setTimeout(() => {
      alert('Sent!');
      setIsSending(false);
    }, 3000);
  }

  function handleUndo() {
    setIsSending(false);
    clearTimeout(timeoutID);
  }

  return (
    <>
      <input
        disabled={isSending}
        value={text}
        onChange={e => setText(e.target.value)}
      />
      <button
        disabled={isSending}
        onClick={handleSend}>
        {isSending ? 'Sending...' : 'Send'}
      </button>
      {isSending &&
        <button onClick={handleUndo}>
          Undo
        </button>
      }
    </>
  );
}

© 2013–present Facebook Inc.
Licensed under the Creative Commons Attribution 4.0 International Public License.
https://react.dev/learn/referencing-values-with-refs

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API