Перейти к содержанию

Манипулирование DOM с помощью Refs

React автоматически обновляет DOM в соответствии с вашим выводом на экран, поэтому вашим компонентам не часто требуется манипулировать им. Однако иногда вам может понадобиться доступ к элементам DOM, управляемым React - например, для фокусировки узла, прокрутки к нему или измерения его размера и положения. Встроенного способа сделать это в React нет, поэтому вам понадобится ref на узел DOM.

  • Как получить доступ к узлу DOM, управляемому React, с помощью атрибута ref
  • Как JSX-атрибут ref связан с хуком useRef.
  • Как получить доступ к DOM-узлу другого компонента
  • В каких случаях безопасно изменять DOM под управлением React

Получение ссылки на узел {/getting-a-ref-to-the-node/}

Чтобы получить доступ к узлу DOM, управляемому React, сначала импортируйте хук useRef:

1
import { useRef } from 'react';

Затем используйте его для объявления ссылки внутри вашего компонента:

1
const myRef = useRef(null);

Наконец, передайте его узлу DOM в качестве атрибута ref:

1
<div ref={myRef}>

The useRef Hook returns an object with a single property called current. Initially, myRef.current will be null. When React creates a DOM node for this <div>, React will put a reference to this node into myRef.current. You can then access this DOM node from your event handlers and use the built-in browser APIs defined on it.

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

Пример: Фокусировка текстового ввода {/example-focusing-a-text-input/}

В этом примере нажатие на кнопку фокусирует ввод:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
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>
        </>
    );
}

To implement this:

  1. Declare inputRef with the useRef Hook.
  2. Pass it as <input ref={inputRef}>. This tells React to put this <input>’s DOM node into inputRef.current.
  3. In the handleClick function, read the input DOM node from inputRef.current and call focus() on it with inputRef.current.focus().
  4. Pass the handleClick event handler to <button> with onClick.

While DOM manipulation is the most common use case for refs, the useRef Hook can be used for storing other things outside React, like timer IDs. Similarly to state, refs remain between renders. Refs are like state variables that don’t trigger re-renders when you set them. Read about refs in Referencing Values with Refs.

Example: Scrolling to an element {/example-scrolling-to-an-element/}

You can have more than a single ref in a component. In this example, there is a carousel of three images. Each button centers an image by calling the browser scrollIntoView() method on the corresponding DOM node:

 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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
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>
        </>
    );
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
div {
    width: 100%;
    overflow: hidden;
}

nav {
    text-align: center;
}

button {
    margin: 0.25rem;
}

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

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

Как управлять списком ссылок с помощью обратного вызова {/how-to-manage-a-list-of-refs-using-a-ref-callback/}

В приведенных выше примерах существует предопределенное количество ссылок. Однако иногда вам может понадобиться ссылка на каждый элемент списка, и вы не знаете, сколько их будет. Что-то вроде этого не будет работать:

1
2
3
4
5
6
7
<ul>
    {items.map((item) => {
        // Doesn't work!
        const ref = useRef(null);
        return <li ref={ref} />;
    })}
</ul>

Это происходит потому, что Хуки должны вызываться только на верхнем уровне вашего компонента. Вы не можете вызвать useRef в цикле, в условии или внутри вызова map().

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

Другое решение - передать функцию атрибуту ref. Это называется ref callback. React вызовет ваш ref callback с узлом DOM, когда придет время установить ref, и с null, когда придет время очистить его. Это позволяет вам вести свой собственный массив или Map, и обращаться к любому 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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
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,
    });
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
div {
    width: 100%;
    overflow: hidden;
}

nav {
    text-align: center;
}

button {
    margin: 0.25rem;
}

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

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

В этом примере itemsRef не содержит ни одного узла DOM. Вместо этого он содержит Map от ID элемента к узлу DOM. (Ссылки могут содержать любые значения!) Обратный вызов ref для каждого элемента списка заботится об обновлении карты:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
<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);
    }
  }}
>

This lets you read individual DOM nodes from the Map later.

Accessing another component’s DOM nodes {/accessing-another-components-dom-nodes/}

When you put a ref on a built-in component that outputs a browser element like <input />, React will set that ref’s current property to the corresponding DOM node (such as the actual <input /> in the browser).

However, if you try to put a ref on your own component, like <MyInput />, by default you will get null. Here is an example demonstrating it. Notice how clicking the button does not focus the input:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
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 может использовать API forwardRef:

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

This is how it works:

  1. <MyInput ref={inputRef} /> tells React to put the corresponding DOM node into inputRef.current. However, it’s up to the MyInput component to opt into that–by default, it doesn’t.
  2. The MyInput component is declared using forwardRef. This opts it into receiving the inputRef from above as the second ref argument which is declared after props.
  3. MyInput itself passes the ref it received to the <input> inside of it.

Now clicking the button to focus the input works:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
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 с помощью императивного дескриптора {/exposing-a-subset-of-the-api-with-an-imperative-handle/}

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

 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
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. В этом случае ref "handle" - это не узел DOM, а пользовательский объект, который вы создаете внутри вызова useImperativeHandle.

Когда React присоединяет ссылки {/when-react-attaches-the-refs/}

В React каждое обновление делится на две фазы:

  • Во время render, React вызывает ваши компоненты, чтобы выяснить, что должно быть на экране.
  • Во время commit, React применяет изменения в DOM.

В общем, вы не хотите обращаться к рефкам во время рендеринга. Это относится и к ссылкам, содержащим узлы DOM. Во время первого рендеринга узлы DOM еще не были созданы, поэтому ref.current будет null. А во время рендеринга обновлений, узлы DOM еще не были обновлены. Поэтому читать их еще рано.

React устанавливает ref.current во время фиксации. Перед обновлением DOM, React устанавливает затронутые значения ref.current в null. После обновления DOM, React немедленно устанавливает их в соответствующие узлы DOM.

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

Промывка обновлений состояния синхронно с помощью flushSync {/flush-state-updates-synchronously-with-flush-sync/}

Рассмотрим код, подобный этому, который добавляет новый todo и прокручивает экран вниз до последнего дочернего элемента списка. Обратите внимание, что по какой-то причине он всегда прокручивается к тому todo, который был прямо перед последним добавленным:

 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
40
41
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),
    });
}

Проблема заключается в этих двух линиях:

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

В React обновления состояния ставятся в очередь. Обычно это то, что вам нужно. Однако здесь это вызывает проблему, потому что setTodos не обновляет DOM немедленно. Поэтому, когда вы прокручиваете список до последнего элемента, todo еще не был добавлен. Поэтому прокрутка всегда "отстает" на один элемент.

Чтобы решить эту проблему, вы можете заставить React обновлять ("промывать") DOM синхронно. Для этого импортируйте flushSync из react-dom и оберните обновление состояния в вызов flushSync:

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

Это даст команду React синхронно обновить DOM сразу после выполнения кода, обернутого в flushSync. В результате, последний todo уже будет в DOM к тому моменту, когда вы попытаетесь перейти к нему:

 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
40
41
42
43
44
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 с помощью ссылок {/best-practices-for-dom-manipulation-with-refs/}

Ссылки - это аварийный люк. Вы должны использовать их только тогда, когда вам нужно "выйти за пределы React". Обычные примеры этого - управление фокусом, позицией прокрутки или вызов API браузера, которые React не раскрывает.

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

Чтобы проиллюстрировать эту проблему, данный пример включает в себя приветственное сообщение и две кнопки. Первая кнопка переключает свое присутствие, используя conditional rendering и state, как вы обычно делаете в React. Вторая кнопка использует remove() DOM API, чтобы принудительно удалить ее из DOM вне контроля React.

Попробуйте нажать "Toggle with setState" несколько раз. Сообщение должно исчезнуть и появиться снова. Затем нажмите "Удалить из DOM". Это приведет к принудительному удалению. Наконец, нажмите "Toggle with setState":

 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
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>
    );
}
1
2
3
4
5
p,
button {
    display: block;
    margin: 10px;
}

After you’ve manually removed the DOM element, trying to use setState to show it again will lead to a crash. This is because you’ve changed the DOM, and React doesn’t know how to continue managing it correctly.

Avoid changing DOM nodes managed by React. Modifying, adding children to, or removing children from elements that are managed by React can lead to inconsistent visual results or crashes like above.

However, this doesn’t mean that you can’t do it at all. It requires caution. You can safely modify parts of the DOM that React has no reason to update. For example, if some <div> is always empty in the JSX, React won’t have a reason to touch its children list. Therefore, it is safe to manually add or remove elements there.

\<Recap>

  • Refs are a generic concept, but most often you’ll use them to hold DOM elements.
  • You instruct React to put a DOM node into myRef.current by passing <div ref={myRef}>.
  • Usually, you will use refs for non-destructive actions like focusing, scrolling, or measuring DOM elements.
  • A component doesn’t expose its DOM nodes by default. You can opt into exposing a DOM node by using forwardRef and passing the second ref argument down to a specific node.
  • Avoid changing DOM nodes managed by React.
  • Если вы изменяете узлы DOM, управляемые React, изменяйте те части, которые React не имеет причин обновлять.

\</Recap>

\<Проблемы>

Воспроизведение и пауза видео {/play-and-pause-the-video/}

В этом примере кнопка переключает переменную состояния для перехода между воспроизведением и паузой. Однако для того, чтобы действительно воспроизвести или поставить видео на паузу, переключения состояния недостаточно. Вам также необходимо вызвать play() и pause() на элементе DOM для <video>. Добавьте к нему ссылку и заставьте кнопку работать.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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>
        </>
    );
}
1
2
3
4
button {
    display: block;
    margin-bottom: 20px;
}

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

\<Решение>

Объявите ссылку и поместите ее на элемент <video>. Затем вызовите ref.current.play() и ref.current.pause() в обработчике событий в зависимости от следующего состояния.

 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
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>
        </>
    );
}
1
2
3
4
button {
    display: block;
    margin-bottom: 20px;
}

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

\</Solution>

Фокусировка поля поиска {/focus-the-search-field/}

Сделайте так, чтобы нажатие на кнопку "Поиск" наводило фокус на поле.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
export default function Page() {
    return (
        <>
            <nav>
                <button>Search</button>
            </nav>
            <input placeholder="Looking for something?" />
        </>
    );
}
1
2
3
4
button {
    display: block;
    margin-bottom: 10px;
}

\<Решение>

Добавьте ссылку на вход и вызовите focus() на узле DOM, чтобы сфокусировать его:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
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?"
            />
        </>
    );
}
1
2
3
4
button {
    display: block;
    margin-bottom: 10px;
}

\</Solution>

Эта карусель изображений имеет кнопку "Next", которая переключает активное изображение. Заставьте галерею прокручиваться горизонтально до активного изображения по щелчку. Для этого нужно вызвать scrollIntoView() на DOM-узле активного изображения:

1
2
3
4
5
node.scrollIntoView({
    behavior: 'smooth',
    block: 'nearest',
    inline: 'center',
});

\< Подсказка>

Для этого упражнения не обязательно иметь ссылку на каждое изображение. Достаточно иметь ссылку на текущее активное изображение или на сам список. Используйте flushSync для обеспечения обновления DOM до прокрутки.

\</Hint>

 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
40
41
42
43
44
45
46
47
48
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,
    });
}
 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
div {
    width: 100%;
    overflow: hidden;
}

nav {
    text-align: center;
}

button {
    margin: 0.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, а затем передать его условно только текущему изображению:

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

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

Обратите внимание, что вызов flushSync необходим для того, чтобы заставить React обновить DOM перед прокруткой. В противном случае selectedRef.current всегда будет указывать на ранее выбранный элемент.

 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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
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,
    });
}
 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
div {
    width: 100%;
    overflow: hidden;
}

nav {
    text-align: center;
}

button {
    margin: 0.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);
}

\</Solution>

Фокусировка поля поиска с помощью отдельных компонентов {/focus-the-search-field-with-separate-components/}

Сделайте так, чтобы нажатие на кнопку "Поиск" наводило фокус на поле. Обратите внимание, что каждый компонент определен в отдельном файле и не должен быть перемещен из него. Как соединить их вместе?

\<Hint>

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

\</Hint>

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
import SearchButton from './SearchButton.js';
import SearchInput from './SearchInput.js';

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

\<Solution>

You’ll need to add an onClick prop to the SearchButton, and make the SearchButton pass it down to the browser <button>. You’ll also pass a ref down to <SearchInput>, which will forward it to the real <input> and populate it. Finally, in the click handler, you’ll call focus on the DOM node stored inside that ref.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
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} />
        </>
    );
}
1
2
3
export default function SearchButton({ onClick }) {
    return <button onClick={onClick}>Search</button>;
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
import { forwardRef } from 'react';

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

\</Solution>

\</Challenges>

Комментарии