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

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

  • Як отримати доступ до DOM-вузла, керованого React за допомогою атрибуту ref
  • Як атрибут ref JSX пов'язаний з useRef хуком
  • Як отримати доступ до DOM-вузла іншого компонента
  • У яких випадках безпечно змінювати DOM, керований React
  • .

Отримання посилання на вузол

.

Щоб отримати доступ до DOM-вузла, керованого React, спочатку імпортуйте хук useRef Hook:

import { useRef } from 'react';

Потім використовуйте його для оголошення рефа всередині вашого компонента:

const myRef = useRef(null);

Нарешті, передайте свій реф як атрибут ref до JSX-тегу, для якого ви хочете отримати вузол DOM:

<div ref={myRef}>

Хук useRef повертає об'єкт з єдиною властивістю current. Спочатку myRef.current буде null. Коли React створить DOM-вузол для цього <div>, React помістить посилання на цей вузол в myRef.current. Після цього ви можете отримати доступ до цього DOM-вузла з ваших обробників подій і використовувати вбудовані браузерні API, визначені на ньому.

// You can use any browser APIs, for example:
myRef.current.scrollIntoView();

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

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

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

Щоб реалізувати це:

  1. Оголосити inputRef за допомогою useRef хука.
  2. Передайте його як <input ref={inputRef}>. Це скаже React'у помістити DOM-вузол цього <input> у inputRef.current.
  3. У функції handleClick прочитайте вхідний вузол DOM з inputRef.current і викличте на ньому focus() з inputRef.current.focus().
  4. Передати обробник події handleClick до <button> з onClick.

Хоча маніпуляції з DOM є найбільш поширеним випадком використання рефів, хук useRef можна використовувати для зберігання інших речей поза React, наприклад, ідентифікаторів таймерів. Подібно до стану, рефи залишаються між рендерингами. Рефи подібні до змінних стану, які не викликають повторних рендерингів, коли ви їх встановлюєте. Читайте про рефи у статті Посилання на значення за допомогою рефів.

Приклад: Прокрутка до елемента

.

У компоненті може бути більше одного рефа. У цьому прикладі є карусель з трьох зображень. Кожна кнопка центрує зображення шляхом виклику методу браузера scrollIntoView() на відповідному DOM-вузлі:

import { useRef } from 'react';

export default function CatFriends() {
  const firstCatRef = useRef(null);
  const secondCatRef = useRef(null);
  const thirdCatRef = useRef(null);

  function handleScrollToFirstCat() {
    firstCatRef.current.scrollIntoView({
      behavior: 'smooth',
      block: 'nearest',
      inline: 'center'
    });
  }

  function handleScrollToSecondCat() {
    secondCatRef.current.scrollIntoView({
      behavior: 'smooth',
      block: 'nearest',
      inline: 'center'
    });
  }

  function handleScrollToThirdCat() {
    thirdCatRef.current.scrollIntoView({
      behavior: 'smooth',
      block: 'nearest',
      inline: 'center'
    });
  }

  return (
    <>
      <nav>
        <button onClick={handleScrollToFirstCat}>
          Tom
        </button>
        <button onClick={handleScrollToSecondCat}>
          Maru
        </button>
        <button onClick={handleScrollToThirdCat}>
          Jellylorum
        </button>
      </nav>
      <div>
        <ul>
          <li>
            <img
              src="https://placekitten.com/g/200/200"
              alt="Tom"
              ref={firstCatRef}
            />
          </li>
          <li>
            <img
              src="https://placekitten.com/g/300/200"
              alt="Maru"
              ref={secondCatRef}
            />
          </li>
          <li>
            <img
              src="https://placekitten.com/g/250/200"
              alt="Jellylorum"
              ref={thirdCatRef}
            />
          </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;
}

Як керувати списком рефів за допомогою зворотного виклику рефів

У наведених вище прикладах є наперед визначена кількість посилань. Однак іноді вам може знадобитися посилання на кожен елемент у списку, і ви не знаєте, скільки їх у вас буде. Щось на зразок цього не спрацює:

<ul>
  {items.map((item) => {
    // Doesn't work!
    const ref = useRef(null);
    return <li ref={ref} />;
  })}
</ul>

Це тому, що хуки слід викликати лише на верхньому рівні вашого компонента. Ви не можете викликати useRef у циклі, в умові або всередині map() виклику.

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

Іншим рішенням є передача функції до атрибуту ref. Це називається зворотнім викликом ref. React буде викликати ваш зворотній виклик з DOM-вузла, коли прийде час встановити реф, і з null, коли прийде час його очистити. Це дозволяє вам підтримувати власний масив або Map, і отримувати доступ до будь-якого рефа за його індексом або якимось ідентифікатором.

У цьому прикладі показано, як можна використовувати цей підхід для прокрутки до довільного вузла у довгому списку:

import { useRef } from 'react';

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

  function scrollToId(itemId) {
    const map = getMap();
    const node = map.get(itemId);
    node.scrollIntoView({
      behavior: 'smooth',
      block: 'nearest',
      inline: 'center'
    });
  }

  function getMap() {
    if (!itemsRef.current) {
      // Initialize the Map on first usage.
      itemsRef.current = new Map();
    }
    return itemsRef.current;
  }

  return (
    <>
      <nav>
        <button onClick={() => scrollToId(0)}>
          Tom
        </button>
        <button onClick={() => scrollToId(5)}>
          Maru
        </button>
        <button onClick={() => scrollToId(9)}>
          Jellylorum
        </button>
      </nav>
      <div>
        <ul>
          {catList.map(cat => (
            <li
              key={cat.id}
              ref={(node) => {
                const map = getMap();
                if (node) {
                  map.set(cat.id, node);
                } else {
                  map.delete(cat.id);
                }
              }}
            >
              <img
                src={cat.imageUrl}
                alt={'Cat #' + cat.id}
              />
            </li>
          ))}
        </ul>
      </div>
    </>
  );
}

const catList = [];
for (let i = 0; i < 10; i++) {
  catList.push({
    id: i,
    imageUrl: 'https://placekitten.com/250/200?image=' + i
  });
}
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;
}

У цьому прикладі itemsRef не містить жодного вузла DOM. Натомість, він містить Map від ідентифікатора елемента до вузла DOM. (Посилання можуть містити будь-які значення!) Зворотний виклик посилання для кожного елемента списку дбає про оновлення мапи:

<li
  key={cat.id}
  ref={node => {
    const map = getMap();
    if (node) {
      // Add to the Map
      map.set(cat.id, node);
    } else {
      // Remove from the Map
      map.delete(cat.id);
    }
  }}
>

Це дозволить вам пізніше зчитувати окремі вузли DOM з мапи.

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

Коли ви ставите реф на вбудований компонент, який виводить елемент браузера на кшталт <input />, React встановить властивість current цього рефа у відповідний вузол DOM (наприклад, фактичний <input /> в браузері).

Втім, якщо ви спробуєте поставити посилання на власний компонент, наприклад <MyInput />, за замовчуванням ви отримаєте null. Ось приклад, що демонструє це. Зверніть увагу, що натискання кнопки не фокусує ввід:

import { useRef } from 'react';

function MyInput(props) {
  return <input {...props} />;
}

export default function MyForm() {
  const inputRef = useRef(null);

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

  return (
    <>
      <MyInput ref={inputRef} />
      <button onClick={handleClick}>
        Focus the input
      </button>
    </>
  );
}

Щоб допомогти вам помітити проблему, React також виводить помилку в консоль:

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

Це відбувається тому, що за замовчуванням React не дозволяє компоненту отримувати доступ до DOM-вузлів інших компонентів. Навіть своїм власним дочірнім компонентам! Це зроблено навмисно. Рефи - це рятівний люк, який слід використовувати обережно. Ручне маніпулювання вузлами DOM іншого компонента робить ваш код ще більш вразливим.

Натомість, компоненти, які хочуть показувати свої вузли DOM, мають погодитися на таку поведінку. Компонент може вказати, що він "пересилає" своє посилання одному зі своїх нащадків. Ось як MyInput може використовувати forwardRef API:

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

Ось як це працює:

  1. <MyInput ref={inputRef} /> каже React'у помістити відповідний DOM-вузол у inputRef.current. Однак, компонент MyInput сам вирішує, чи робити це. За замовчуванням він цього не робить.
  2. Компонент MyInput оголошено за допомогою forwardRef. Це дозволяє йому отримати inputRef зверху як другий ref аргумент, який оголошено після пропсів.
  3. MyInput сам передає отриманий реф до <input> всередині нього.

Тепер натискання кнопки для фокусування введення працює:

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

У системах проектування низькорівневі компоненти, такі як кнопки, входи тощо, зазвичай пересилають свої посилання на вузли DOM. З іншого боку, високорівневі компоненти, такі як форми, списки або розділи сторінок, зазвичай не показують свої DOM-вузли, щоб уникнути випадкових залежностей від структури DOM.

Відкриття підмножини API з імперативним дескриптором

У наведеному вище прикладі MyInput розкриває оригінальний елемент вводу DOM. Це дозволяє батьківському компоненту викликати на ньому focus(). Однак, це також дозволяє батьківському компоненту робити щось інше - наприклад, змінювати свої стилі CSS. У рідкісних випадках вам може знадобитися обмежити відкриту функціональність. Ви можете зробити це за допомогою useImperativeHandle:

import {
  forwardRef, 
  useRef, 
  useImperativeHandle
} from 'react';

const MyInput = forwardRef((props, ref) => {
  const realInputRef = useRef(null);
  useImperativeHandle(ref, () => ({
    // Only expose focus and nothing else
    focus() {
      realInputRef.current.focus();
    },
  }));
  return <input {...props} ref={realInputRef} />;
});

export default function Form() {
  const inputRef = useRef(null);

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

  return (
    <>
      <MyInput ref={inputRef} />
      <button onClick={handleClick}>
        Focus the input
      </button>
    </>
  );
}

Тут realInputRef всередині MyInput міститься власне вхідний вузол DOM. Однак, useImperativeHandle вказує React надати ваш власний спеціальний об'єкт як значення посилання на батьківський компонент. Таким чином, inputRef.current всередині компонента Form матиме лише метод focus. У цьому випадку реф "handle" - це не вузол DOM, а користувацький об'єкт, який ви створюєте всередині виклику useImperativeHandle.

Коли React приєднує посилання

У React кожне оновлення розбивається на дві фази:

  • Під час рендерингу, React викликає ваші компоненти, щоб зрозуміти, що має бути на екрані.
  • Під час фіксації, React застосовує зміни до DOM.

Загалом, ви не хочете отримувати доступ до рефів під час рендерингу. Це стосується і рефів, що містять вузли DOM. Під час першого рендеру DOM-вузли ще не створено, тому ref.current буде null. А під час рендерингу оновлень вузли DOM ще не були оновлені. Тому читати їх ще зарано.

React встановлює ref.current під час комміту. Перед оновленням DOM, React встановлює значення ref.current як null. Після оновлення DOM, React негайно встановлює їх у відповідні вузли DOM.

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

Стан промивання оновлюється синхронно з flushSync

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

import { useState, useRef } from 'react';

export default function TodoList() {
  const listRef = useRef(null);
  const [text, setText] = useState('');
  const [todos, setTodos] = useState(
    initialTodos
  );

  function handleAdd() {
    const newTodo = { id: nextId++, text: text };
    setText('');
    setTodos([ ...todos, newTodo]);
    listRef.current.lastChild.scrollIntoView({
      behavior: 'smooth',
      block: 'nearest'
    });
  }

  return (
    <>
      <button onClick={handleAdd}>
        Add
      </button>
      <input
        value={text}
        onChange={e => setText(e.target.value)}
      />
      <ul ref={listRef}>
        {todos.map(todo => (
          <li key={todo.id}>{todo.text}</li>
        ))}
      </ul>
    </>
  );
}

let nextId = 0;
let initialTodos = [];
for (let i = 0; i < 20; i++) {
  initialTodos.push({
    id: nextId++,
    text: 'Todo #' + (i + 1)
  });
}

Проблема у цих двох рядках:

setTodos([ ...todos, newTodo]);
listRef.current.lastChild.scrollIntoView();

У React оновлення стану ставляться в чергу. Зазвичай, це те, чого ви хочете. Однак тут це викликає проблему, тому що setTodos не оновлює DOM одразу. Отже, коли ви прокручуєте список до останнього елемента, справа ще не буде додана. Ось чому прокрутка завжди "відстає" на один елемент.

Щоб виправити цю проблему, ви можете змусити React оновлювати ("відмивати") DOM синхронно. Для цього імпортуйте flushSync з react-dom та обгорніть оновлення стану у виклик flushSync:

flushSync(() => {
  setTodos([ ...todos, newTodo]);
});
listRef.current.lastChild.scrollIntoView();

Це дасть інструкцію React синхронно оновити DOM одразу після виконання коду, обгорнутого в flushSync. В результаті, остання справа вже буде в DOM, коли ви спробуєте до неї перейти:

import { useState, useRef } from 'react';
import { flushSync } from 'react-dom';

export default function TodoList() {
  const listRef = useRef(null);
  const [text, setText] = useState('');
  const [todos, setTodos] = useState(
    initialTodos
  );

  function handleAdd() {
    const newTodo = { id: nextId++, text: text };
    flushSync(() => {
      setText('');
      setTodos([ ...todos, newTodo]);      
    });
    listRef.current.lastChild.scrollIntoView({
      behavior: 'smooth',
      block: 'nearest'
    });
  }

  return (
    <>
      <button onClick={handleAdd}>
        Add
      </button>
      <input
        value={text}
        onChange={e => setText(e.target.value)}
      />
      <ul ref={listRef}>
        {todos.map(todo => (
          <li key={todo.id}>{todo.text}</li>
        ))}
      </ul>
    </>
  );
}

let nextId = 0;
let initialTodos = [];
for (let i = 0; i < 20; i++) {
  initialTodos.push({
    id: nextId++,
    text: 'Todo #' + (i + 1)
  });
}

Найкращі практики маніпулювання DOM з посиланнями

Рефи є аварійним виходом. Ви повинні використовувати їх лише тоді, коли вам потрібно "вийти за межі React". Поширеними прикладами цього є керування фокусом, позицією прокрутки або виклик API браузера, які React не використовує.

Якщо ви дотримуватиметеся неруйнівних дій, таких як фокусування та прокрутка, у вас не повинно виникнути жодних проблем. Однак, якщо ви спробуєте модифікувати DOM вручну, то можете зіткнутися з конфліктом зі змінами, які робить React.

Щоб проілюструвати цю проблему, цей приклад містить привітальне повідомлення і дві кнопки. Перша кнопка вмикає свою присутність за допомогою умовного рендерингу та стану, як ви зазвичай робите в React. Друга кнопка використовує remove() DOM API, щоб примусово видалити її з DOM поза контролем React.

Спробуйте натиснути "Toggle with setState" кілька разів. Повідомлення повинно зникнути і з'явитися знову. Потім натисніть "Remove from the DOM". Це призведе до примусового видалення. Нарешті, натисніть "Toggle with setState":

import { useState, useRef } from 'react';

export default function Counter() {
  const [show, setShow] = useState(true);
  const ref = useRef(null);

  return (
    <div>
      <button
        onClick={() => {
          setShow(!show);
        }}>
        Toggle with setState
      </button>
      <button
        onClick={() => {
          ref.current.remove();
        }}>
        Remove from the DOM
      </button>
      {show && <p ref={ref}>Hello world</p>}
    </div>
  );
}
p,
button {
  display: block;
  margin: 10px;
}

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

Уникайте зміни DOM-вузлів, керованих React. Модифікація, додавання дочірніх елементів або видалення дочірніх елементів, керованих React, може призвести до непослідовних візуальних результатів або збоїв, як описано вище.

Втім, це не означає, що ви не можете зробити це взагалі. Це вимагає обережності. Ви можете безпечно змінювати частини DOM, які React не має причин оновлювати. Наприклад, якщо деякий <div> завжди порожній в JSX, React не матиме причин торкатися його списку дочірніх елементів. Тому ви можете безпечно додавати або видаляти елементи вручну.

  • Рефи є загальним поняттям, але найчастіше ви будете використовувати їх для зберігання елементів DOM.
  • Ви вказуєте React помістити DOM-вузол у myRef.current, передавши <div ref={myRef}>.
  • Зазвичай, ви будете використовувати рефи для неруйнівних дій, таких як фокусування, прокрутка або вимірювання елементів DOM.
  • За замовчуванням компонент не показує свої DOM-вузли. Ви можете ввімкнути показ вузла DOM за допомогою forwardRef і передати другий аргумент ref до конкретного вузла.
  • Уникайте зміни DOM-вузлів, якими керує React.
  • Якщо ви змінюєте вузли DOM, керовані React, змінюйте частини, які React не має причин оновлювати.

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

У цьому прикладі кнопка перемикає змінну стану для перемикання між відтворенням та паузою. Однак, для того, щоб насправді відтворити або поставити відео на паузу, перемикання стану недостатньо. Вам також потрібно викликати play() і pause() на DOM-елементі для <video>. Додайте до нього посилання і змусьте кнопку працювати.

import { useState, useRef } from 'react';

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

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

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

Для додаткового виклику, тримайте кнопку "Відтворити" синхронізованою з тим, чи відтворюється відео, навіть якщо користувач клацає правою кнопкою миші на відео і відтворює його за допомогою вбудованих елементів керування медіа браузера. Для цього ви можете прослухати onPlay та onPause у відео.

Оголосіть реф і помістіть його в елемент <video>. Потім викликайте ref.current.play() та ref.current.pause() в обробнику події залежно від наступного стану.

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

Для обробки вбудованих елементів керування браузера ви можете додати обробники onPlay і onPause до елемента <video> і викликати з них setIsPlaying. Таким чином, якщо користувач відтворюватиме відео за допомогою елементів керування браузера, стан зміниться відповідним чином.

Сфокусувати поле пошуку

Зробіть так, щоб при натисканні кнопки "Пошук" фокус потрапляв у поле.

export default function Page() {
  return (
    <>
      <nav>
        <button>Search</button>
      </nav>
      <input
        placeholder="Looking for something?"
      />
    </>
  );
}
button { display: block; margin-bottom: 10px; }

Додайте посилання на вхід і викличте focus() на вузлі DOM, щоб сфокусувати його:

import { useRef } from 'react';

export default function Page() {
  const inputRef = useRef(null);
  return (
    <>
      <nav>
        <button onClick={() => {
          inputRef.current.focus();
        }}>
          Search
        </button>
      </nav>
      <input
        ref={inputRef}
        placeholder="Looking for something?"
      />
    </>
  );
}
button { display: block; margin-bottom: 10px; }

Ця карусель зображень має кнопку "Далі", яка перемикає активне зображення. Зробіть так, щоб галерея прокручувалася по горизонталі до активного зображення при натисканні. Вам потрібно викликати scrollIntoView() на DOM-вузлі активного зображення:

node.scrollIntoView({
  behavior: 'smooth',
  block: 'nearest',
  inline: 'center'
});

Вам не потрібно мати посилання на кожне зображення для цієї вправи. Достатньо мати посилання на поточне активне зображення або на сам список. Використовуйте flushSync, щоб переконатися, що DOM оновлюється перед прокручуванням.

import { useState } from 'react';

export default function CatFriends() {
  const [index, setIndex] = useState(0);
  return (
    <>
      <nav>
        <button onClick={() => {
          if (index < catList.length - 1) {
            setIndex(index + 1);
          } else {
            setIndex(0);
          }
        }}>
          Next
        </button>
      </nav>
      <div>
        <ul>
          {catList.map((cat, i) => (
            <li key={cat.id}>
              <img
                className={
                  index === i ?
                    'active' :
                    ''
                }
                src={cat.imageUrl}
                alt={'Cat #' + cat.id}
              />
            </li>
          ))}
        </ul>
      </div>
    </>
  );
}

const catList = [];
for (let i = 0; i < 10; i++) {
  catList.push({
    id: i,
    imageUrl: 'https://placekitten.com/250/200?image=' + i
  });
}
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;
}

img {
  padding: 10px;
  margin: -10px;
  transition: background 0.2s linear;
}

.active {
  background: rgba(0, 100, 150, 0.4);
}

Ви можете оголосити selectedRef, а потім умовно передати його лише до поточного зображення:

<li ref={index === i ? selectedRef : null}>

Коли index === i, що означає, що зображення є вибраним, <li> отримає selectedRef. React переконається, що selectedRef.current завжди вказує на правильний вузол DOM.

Зверніть увагу, що виклик flushSync необхідний, щоб змусити React оновити DOM перед скроллом. Інакше selectedRef.current завжди вказуватиме на попередньо вибраний елемент.

import { useRef, useState } from 'react';
import { flushSync } from 'react-dom';

export default function CatFriends() {
  const selectedRef = useRef(null);
  const [index, setIndex] = useState(0);

  return (
    <>
      <nav>
        <button onClick={() => {
          flushSync(() => {
            if (index < catList.length - 1) {
              setIndex(index + 1);
            } else {
              setIndex(0);
            }
          });
          selectedRef.current.scrollIntoView({
            behavior: 'smooth',
            block: 'nearest',
            inline: 'center'
          });            
        }}>
          Next
        </button>
      </nav>
      <div>
        <ul>
          {catList.map((cat, i) => (
            <li
              key={cat.id}
              ref={index === i ?
                selectedRef :
                null
              }
            >
              <img
                className={
                  index === i ?
                    'active'
                    : ''
                }
                src={cat.imageUrl}
                alt={'Cat #' + cat.id}
              />
            </li>
          ))}
        </ul>
      </div>
    </>
  );
}

const catList = [];
for (let i = 0; i < 10; i++) {
  catList.push({
    id: i,
    imageUrl: 'https://placekitten.com/250/200?image=' + i
  });
}
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;
}

img {
  padding: 10px;
  margin: -10px;
  transition: background 0.2s linear;
}

.active {
  background: rgba(0, 100, 150, 0.4);
}

Фокусування поля пошуку за допомогою окремих компонентів

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

Вам знадобиться forwardRef, щоб вибрати показ DOM-вузла з вашого власного компонента, наприклад SearchInput.

.
import SearchButton from './SearchButton.js';
import SearchInput from './SearchInput.js';

export default function Page() {
  return (
    <>
      <nav>
        <SearchButton />
      </nav>
      <SearchInput />
    </>
  );
}
export default function SearchButton() {
  return (
    <button>
      Search
    </button>
  );
}
export default function SearchInput() {
  return (
    <input
      placeholder="Looking for something?"
    />
  );
}
button { display: block; margin-bottom: 10px; }

Вам потрібно додати проп onClick до SearchButton, і змусити SearchButton передати його браузеру <button>. Ви також передасте посилання до <SearchInput>, який перенаправить його до реального <input> і заповнить його. Нарешті, в обробнику кліку ви викличете focus на DOM-вузол, що зберігається у цьому рефі.

import { useRef } from 'react';
import SearchButton from './SearchButton.js';
import SearchInput from './SearchInput.js';

export default function Page() {
  const inputRef = useRef(null);
  return (
    <>
      <nav>
        <SearchButton onClick={() => {
          inputRef.current.focus();
        }} />
      </nav>
      <SearchInput ref={inputRef} />
    </>
  );
}
export default function SearchButton({ onClick }) {
  return (
    <button onClick={onClick}>
      Search
    </button>
  );
}
import { forwardRef } from 'react';

export default forwardRef(
  function SearchInput(props, ref) {
    return (
      <input
        ref={ref}
        placeholder="Looking for something?"
      />
    );
  }
);
button { display: block; margin-bottom: 10px; }