useRef

useRef - це хук React, який дозволяє вам посилатися на значення, яке не потрібне для рендерингу.

const ref = useRef(initialValue)

Довідник

useRef(initialValue)

Викличте useRef на верхньому рівні вашого компонента, щоб оголосити посилання.

import { useRef } from 'react';

function MyComponent() {
  const intervalRef = useRef(0);
  const inputRef = useRef(null);
  // ...

Дивіться більше прикладів нижче.

Параметри

  • initialValue: Значення, яке ви хочете, щоб властивість current об'єкта-посилання мала як початкове значення. Це може бути значення будь-якого типу. Цей аргумент ігнорується після початкового рендеру.

Повернення

useRef повертає об'єкт з однією властивістю:

  • current: Початково встановлено як initialValue, який ви пройшли. Пізніше ви можете змінити його на щось інше. Якщо ви передасте React об'єкт реф як атрибут ref вузлу JSX, React встановить його властивість current.

На наступних рендерингах useRef повертатиме той самий об'єкт.

Застереження

  • Властивість ref.current можна мутувати. На відміну від стану, її можна змінювати. Однак, якщо вона містить об'єкт, який використовується для рендерингу (наприклад, фрагмент вашого стану), то вам не слід мутувати цей об'єкт.
  • Коли ви змінюєте властивість ref.current, React не перерендерить ваш компонент. React не знає, коли ви його змінюєте, тому що посилання - це звичайний JavaScript-об'єкт.
  • Не записуйте та не зчитуйте ref.current під час рендерингу, окрім ініціалізації. Це робить поведінку вашого компонента непередбачуваною.
  • У суворому режимі React викличе вашу функцію компонента двічі для того, щоб допомогти вам знайти випадкові домішки. Це поведінка лише для розробки і не впливає на виробництво. Кожен об'єкт ref буде створено двічі, але одну з версій буде відкинуто. Якщо ваша функція компонента є чистою (як і має бути), це не повинно вплинути на поведінку.

Використання

Посилання на значення за допомогою рефів

.

Викличте useRef на верхньому рівні вашого компонента, щоб оголосити одне або декілька посилань.

import { useRef } from 'react';

function Stopwatch() {
  const intervalRef = useRef(0);
  // ...

useRef повертає об'єкт ref</CodeStep> з єдиною властивістю <CodeStep data-step="2"><code>current спочатку встановлено у початкове значення </CodeStep> який ви надали.</p> <p>На наступних рендерингах <code>useRef повертатиме той самий об'єкт. Ви можете змінити його властивість current, щоб зберегти інформацію і прочитати її пізніше. Це може нагадати вам стан, але є важлива відмінність.

Зміна рефа не призводить до перерендерингу. Це означає, що рефи ідеально підходять для зберігання інформації, яка не впливає на візуальне виведення вашого компонента. Наприклад, якщо вам потрібно зберегти ідентифікатор інтервалу і отримати його пізніше, ви можете помістити його у реф. Щоб оновити значення всередині рефа, потрібно вручну змінити його властивість <code>current :

function handleStartClick() {
  const intervalId = setInterval(() => {
    // ...
  }, 1000);
  intervalRef.current = intervalId;
}

Пізніше ви зможете прочитати ідентифікатор інтервалу з посилання, щоб викликати очистити цей інтервал:

function handleStopClick() {
  const intervalId = intervalRef.current;
  clearInterval(intervalId);
}

Використовуючи посилання, ви гарантуєте, що:

  • Ви можете зберігати інформацію між відображеннями (на відміну від звичайних змінних, які скидаються при кожному рендері).
  • Зміна не спричиняє перевідображення (на відміну від змінних стану, які спричиняють перевідображення).
  • Інформація є локальною для кожної копії вашого компонента (на відміну від зовнішніх змінних, які є спільними).

Зміна рефа не спричиняє перерендерингу, тому рефи не підходять для зберігання інформації, яку ви хочете вивести на екран. Замість цього використовуйте стан. Докладніше про вибір між useRef та useState.

Лічильник кліків

Цей компонент використовує реф для відстеження кількості натискань на кнопку. Зверніть увагу, що тут можна використовувати реф замість стану, оскільки кількість кліків читається і записується лише в обробнику подій.

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.current} в JSX, номер не буде оновлюватися при кліці. Це пов'язано з тим, що значення ref.current не викликає повторного рендерингу. Інформація, яка використовується для рендерингу, повинна бути станом.

Секундомір

У цьому прикладі використовується комбінація стану та посилань. І startTime, і now є змінними стану, оскільки вони використовуються для рендерингу. Але нам також потрібно тримати ідентифікатор інтервалу, щоб ми могли зупинити інтервал при натисканні кнопки. Оскільки ідентифікатор інтервалу не використовується для рендерингу, доцільно зберігати його у рефі та оновлювати вручну.

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.current під час рендерингу.

React очікує, що тіло вашого компонента поводиться як чиста функція:

  • Якщо вхідні дані (пропси, стан та контекст) є однаковими, вона має повернути точно такий самий JSX.
  • Виклик його в іншому порядку або з іншими аргументами не повинен впливати на результати інших викликів.

Читання або запис рефа під час рендерингу порушує ці очікування.

function MyComponent() {
  // ...
  // 🚩 Don't write a ref during rendering
  myRef.current = 123;
  // ...
  // 🚩 Don't read a ref during rendering
  return <h1>{myOtherRef.current}</h1>;
}

Замість цього можна читати або записувати посилання з обробників подій або ефектів.

function MyComponent() {
  // ...
  useEffect(() => {
    // ✅ You can read or write refs in effects
    myRef.current = 123;
  });
  // ...
  function handleClick() {
    // ✅ You can read or write refs in event handlers
    doSomething(myOtherRef.current);
  }
  // ...
}

Якщо вам необхідно прочитати або записати щось під час рендерингу, використовуйте стан замість цього.

Коли ви порушуєте ці правила, ваш компонент може продовжувати працювати, але більшість нових функцій, які ми додаємо до React, будуть покладатися на ці очікування. Дізнайтеся більше про дотримання чистоти ваших компонентів.


Маніпулювання DOM за допомогою посилання

Особливо часто використовують реф для маніпулювання DOM. React має вбудовану підтримку для цього.

Спочатку оголосіть об'єкт ref</CodeStep> з <CodeStep data-step="3">початкове значення</CodeStep> з <коду>null:

import { useRef } from 'react';

function MyComponent() {
  const inputRef = useRef(null);
  // ...

Потім передайте ваш об'єкт рефа як атрибут ref до JSX вузла DOM, яким ви хочете маніпулювати:

// ...
  return <input ref={inputRef} />;

Після того, як React створить DOM-вузол і виведе його на екран, React встановить властивість <code>current вашого об'єкта-посилання до цього DOM-вузла. Тепер ви можете отримати доступ до DOM-вузла <input> і викликати такі методи, як focus():

function handleClick() {
    inputRef.current.focus();
  }

React поверне властивість current як null, коли вузол буде видалено з екрану.

Детальніше про маніпуляції з DOM за допомогою посилань.

Фокусування текстового введення

У цьому прикладі натискання кнопки сфокусує введення:

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>
    </>
  );
}

Прокрутка зображення у вікні перегляду

У цьому прикладі натискання на кнопку прокручує зображення. Він використовує посилання на вузол списку DOM, а потім викликає API DOM querySelectorAll, щоб знайти зображення, до якого ми хочемо прокрутити.

import { useRef } from 'react';

export default function CatFriends() {
  const listRef = useRef(null);

  function scrollToIndex(index) {
    const listNode = listRef.current;
    // This line assumes a particular DOM structure:
    const imgNode = listNode.querySelectorAll('li > img')[index];
    imgNode.scrollIntoView({
      behavior: 'smooth',
      block: 'nearest',
      inline: 'center'
    });
  }

  return (
    <>
      <nav>
        <button onClick={() => scrollToIndex(0)}>
          Tom
        </button>
        <button onClick={() => scrollToIndex(1)}>
          Maru
        </button>
        <button onClick={() => scrollToIndex(2)}>
          Jellylorum
        </button>
      </nav>
      <div>
        <ul ref={listRef}>
          <li>
            <img
              src="https://placekitten.com/g/200/200"
              alt="Tom"
            />
          </li>
          <li>
            <img
              src="https://placekitten.com/g/300/200"
              alt="Maru"
            />
          </li>
          <li>
            <img
              src="https://placekitten.com/g/250/200"
              alt="Jellylorum"
            />
          </li>
        </ul>
      </div>
    </>
  );
}
div {
  width: 100%;
  overflow: hidden;
}

nav {
  text-align: center;
}

button {
  margin: .25rem;
}

ul,
li {
  list-style: none;
  white-space: nowrap;
}

li {
  display: inline;
  padding: 0.5rem;
}

Відтворення та призупинення відео

Цей приклад використовує реф для виклику play() та pause() на DOM-вузлі <video>.

import { useState, useRef } from 'react';

export default function VideoPlayer() {
  const [isPlaying, setIsPlaying] = useState(false);
  const ref = useRef(null);

  function handleClick() {
    const nextIsPlaying = !isPlaying;
    setIsPlaying(nextIsPlaying);

    if (nextIsPlaying) {
      ref.current.play();
    } else {
      ref.current.pause();
    }
  }

  return (
    <>
      <button onClick={handleClick}>
        {isPlaying ? 'Pause' : 'Play'}
      </button>
      <video
        width="250"
        ref={ref}
        onPlay={() => setIsPlaying(true)}
        onPause={() => setIsPlaying(false)}
      >
        <source
          src="https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4"
          type="video/mp4"
        />
      </video>
    </>
  );
}
button { display: block; margin-bottom: 20px; }

Виявлення посилання на власний компонент

Іноді вам може знадобитися дозволити батьківському компоненту маніпулювати DOM всередині вашого компонента. Наприклад, ви пишете компонент MyInput, але хочете, щоб батьківський компонент міг фокусувати введення (до якого батьківський компонент не має доступу). Ви можете використати комбінацію useRef, щоб утримувати введення, і forwardRef, щоб показати його батьківському компоненту. Прочитайте детальний опис тут.

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>
    </>
  );
}

Уникнення відтворення вмісту посилання

React зберігає початкове значення ref один раз і ігнорує його при наступних рендерах.

function Video() {
  const playerRef = useRef(new VideoPlayer());
  // ...

Хоча результат new VideoPlayer() використовується лише для початкового рендерингу, ви все одно викликаєте цю функцію для кожного рендерингу. Це може бути марнотратством, якщо створюються складні об'єкти.

Щоб вирішити цю проблему, ви можете ініціалізувати реф ось так:

function Video() {
  const playerRef = useRef(null);
  if (playerRef.current === null) {
    playerRef.current = new VideoPlayer();
  }
  // ...

Зазвичай запис або читання ref.current під час рендеру заборонено. Однак у цьому випадку це нормально, оскільки результат завжди однаковий, а умова виконується лише під час ініціалізації, тому він повністю передбачуваний.

Як уникнути перевірок на нуль при ініціалізації useRef пізніше

Якщо ви використовуєте перевірку типу і не хочете завжди перевіряти на null, ви можете спробувати такий шаблон:

function Video() {
  const playerRef = useRef(null);

  function getPlayer() {
    if (playerRef.current !== null) {
      return playerRef.current;
    }
    const player = new VideoPlayer();
    playerRef.current = player;
    return player;
  }

  // ...

Тут сам playerRef можна зробити null. Однак, ви повинні бути в змозі переконати вашу програму перевірки типу, що не існує випадку, коли getPlayer() повертає null. Тоді використовуйте getPlayer() у своїх обробниках подій.


Налагодження

Я не можу отримати посилання на користувацький компонент

Якщо ви спробуєте передати посилання у власний компонент ось так:

const inputRef = useRef(null);

return <MyInput ref={inputRef} />;

Ви можете отримати помилку у консолі:

Застереження: Функціональним компонентам не можна давати рефи. Спроби отримати доступ до цього рефу будуть невдалими. Ви хотіли використати React.forwardRef()?

За замовчуванням, ваші власні компоненти не показують посилання на DOM-вузли всередині них.

Щоб виправити це, знайдіть компонент, на який ви хочете отримати посилання:

export default function MyInput({ value, onChange }) {
  return (
    <input
      value={value}
      onChange={onChange}
    />
  );
}

А потім обгорніть його у forwardRef ось так:

import { forwardRef } from 'react';

const MyInput = forwardRef(({ value, onChange }, ref) => {
  return (
    <input
      value={value}
      onChange={onChange}
      ref={ref}
    />
  );
});

export default MyInput;

Тоді батьківський компонент може отримати посилання на нього.

Детальніше про доступ до вузлів DOM іншого компонента.