Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Refs у React (useRef, createref, forwardref)». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Refs у React** дають прямий доступ до DOM-вузла без повторного рендеру. `useRef` зберігає той самий об'єкт між рендерами (функціональні компоненти), `createRef` щоразу створює новий (лише класові компоненти), `forwardRef` передає ref у дочірній компонент. ```jsx const Input = forwardRef((props, ref) => <input ref={ref} {...props} />); function Parent() { const inputRef = useRef(null); return <Input ref={inputRef} />; } ``` **Головне:** доступ до DOM або мутабельні дані без впливу на UI? Ref. Зміни, які бачить користувач? State.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**Refs у React** - це об'єкти, які тримають пряме посилання на DOM-вузол або екземпляр компонента, поза циклом рендерингу. ## Теорія ### TL;DR - Аналогія: ref - це прямий дзвінок до DOM-елемента. Черга рендерингу не задіяна - `useRef` повертає той самий об'єкт `{ current }` при кожному рендері. Зміна `.current` рендер не викликає - `createRef` створює новий об'єкт при кожному виклику, тому підходить лише для конструкторів класових компонентів - `forwardRef` дозволяє батьківському компоненту передати свій ref у дочірній - `useImperativeHandle` контролює, що саме батьківський компонент отримує через цей ref - Правило: треба `focus()`, `play()`, ID таймера або попереднє значення? Використовуй ref. Треба оновити UI? Використовуй state ### Швидкий приклад ```jsx import { useRef } from 'react'; function VideoPlayer() { const videoRef = useRef(null); // починається як { current: null } const play = () => { videoRef.current.play(); // прямий виклик DOM, рендеру немає }; return ( <div> <video ref={videoRef} src="demo.mp4" /> <button onClick={play}>Відтворити</button> </div> ); } ``` Після монтування `videoRef.current` вказує на справжній елемент `<video>`. Кнопка викликає нативний `video.play()` без жодного оновлення стейту. ### Головна відмінність від state Зміна state запускає порівняння віртуального DOM і планує повторний рендер. Ref просто тримає `.current` - звичайну мутабельну властивість, за якою React не стежить. Ти її змінюєш, React не реагує, рендеру немає. React встановлює `ref.current` під час фази фіксації (commit phase), після того як реальний DOM вже оновлено. Тому refs підходять для прямої роботи з DOM або для зберігання мутабельних даних, які не впливають на те, що бачить користувач. ### useRef vs createRef `useRef` зберігає своє значення в `memoizedState` fiber-вузла. Той самий об'єкт повертається при кожному рендері, бо React не скидає хуки під час оновлень. `createRef` кожного разу виділяє новий `{ current: null }`. У функціональному компоненті це означає новий об'єкт при кожному рендері, і ref ніколи не прикріплюється до елемента. ```jsx // Функціональний компонент const ref = useRef(null); // той самий об'єкт при кожному рендері const ref = createRef(); // новий об'єкт при кожному рендері - завжди null ``` `createRef` належить конструкторам класових компонентів, де він виконується рівно один раз. ### Коли використовувати refs - **Фокус і вибір тексту**: `inputRef.current.focus()` після відкриття модального вікна або спрацювання клавіатурного скорочення - **Медіа та canvas**: `videoRef.current.play()`, `canvasRef.current.getContext('2d')` для покадрового малювання - **Мутабельні прапорці без рендеру**: чи вдався автоплей, чи користувач прогорнув секцію, попереднє значення пропса - **ID таймерів**: зберігай повернене значення `setInterval`, щоб викликати `clearInterval` в очищенні `useEffect` - **Сторонні бібліотеки**: Chart.js, Video.js, будь-який canvas-інструмент, якому потрібен реальний DOM-вузол під час ініціалізації Я бачив форми, де стан пагінації зберігали в ref, щоб уникнути зайвих рендерів. Все працювало до моменту, коли треба було відобразити поточну сторінку, і вона завжди показувала 1. Якщо значення впливає на UI, йому місце в state. ### Порівняння API | | `useRef` | `createRef` | `forwardRef` | |---|---|---|---| | Де використовувати | Функціональні компоненти | Класові компоненти | Будь-який компонент, що відкриває ref батьківському | | Зберігається між рендерами | Так | Ні | Залежить від внутрішнього `useRef` | | Викликає рендер | Ні | Ні | Ні | | Типове використання | DOM-доступ, мутабельні значення | DOM-доступ у класових компонентах | Передача ref через межу компонента | | Коли використовувати | Майже завжди | Лише в конструкторах класів | Коли батьківський компонент потребує прямого доступу до DOM дочірнього | ### forwardRef За замовчуванням пропс `ref` на кастомному компоненті просто зникає. React його не передає далі. `forwardRef` обгортає компонент і передає ref другим аргументом у функцію рендерингу: ```jsx import { forwardRef, useRef } from 'react'; const CustomInput = forwardRef((props, ref) => { return <input ref={ref} {...props} />; }); function Parent() { const inputRef = useRef(null); return ( <div> <CustomInput ref={inputRef} placeholder="Введи текст" /> <button onClick={() => inputRef.current.focus()}>Фокус</button> </div> ); } ``` Тепер `inputRef.current` - це справжній `<input>`. Батьківський компонент може викликати будь-який DOM-метод на ньому. ### useImperativeHandle Повний доступ до DOM не завжди бажаний. `useImperativeHandle` дозволяє визначити точно, що отримає батьківський компонент: ```jsx import { forwardRef, useRef, useImperativeHandle } from 'react'; const Video = forwardRef((props, ref) => { const videoRef = useRef(null); useImperativeHandle(ref, () => ({ playPause: () => videoRef.current.paused ? videoRef.current.play() : videoRef.current.pause(), }), []); return <video ref={videoRef} {...props} />; }); function Player() { const videoRef = useRef(null); return ( <> <Video ref={videoRef} src="demo.mp4" /> <button onClick={() => videoRef.current.playPause()}>Перемкнути</button> </> ); } ``` Батьківський компонент отримує лише `playPause`. Реальний DOM-вузол залишається всередині дочірнього. Цей патерн стандартний у бібліотеках компонентів на кшталт React Player. ### Як React прикріплює refs React встановлює `ref.current` під час фази фіксації, після того як завершив запис у реальний DOM. До монтування `ref.current` дорівнює `null`. Після розмонтування React скидає його назад у `null`. Тому читати `ref.current` під час рендерингу - завжди отримуєш застарілий або порожній результат. Читай його всередині `useEffect` або обробників подій. У React 18 з увімкненим StrictMode компоненти монтуються двічі в режимі розробки. `useRef` переживає обидва монтування, бо живе в тому самому fiber-об'єкті. Але ефекти очищаються і запускаються двічі. Якщо зберігаєш observer або підписку в ref - почисти її в функції повернення `useEffect`. ### Типові помилки 1. **Читання `ref.current` під час рендерингу** ```jsx // Неправильно - ref.current тут null, елемента ще немає function Component() { const ref = useRef(null); console.log(ref.current); // null return <div ref={ref}>текст</div>; } // Правильно useEffect(() => { console.log(ref.current); // реальний DOM-вузол }, []); ``` 2. **Зберігання стану UI в ref** ```jsx // Неправильно - відображення застаріває, рендеру немає const valueRef = useRef(''); valueRef.current = e.target.value; // Правильно const [value, setValue] = useState(''); setValue(e.target.value); ``` 3. **`createRef` у функціональному компоненті** ```jsx // Неправильно - новий ref при кожному рендері, ніколи не прикріплюється function Bad() { const ref = createRef(); return <input ref={ref} />; } // Правильно function Good() { const ref = useRef(null); return <input ref={ref} />; } ``` 4. **Відсутня перевірка на null перед використанням `ref.current`** ```jsx // Неправильно - крашнеться, якщо елемент ще не змонтовано inputRef.current.focus(); // Правильно inputRef.current?.focus(); ``` 5. **`forwardRef` без `useImperativeHandle` відкриває весь DOM-вузол** Батьківський компонент може читати `.value`, викликати `.blur()`, напряму змінювати `.style`. Для простого внутрішнього компонента це нормально. Для компонента у спільній бібліотеці або дизайн-системі завжди обмежуй API через `useImperativeHandle`. ### Де зустрічається в реальних проектах - **React Aria (Adobe)**: `useRef` для управління фокусом у доступних (accessible) модальних вікнах і меню - **React Hook Form**: зберігає refs полів для валідації без ре-рендерів на кожне натискання клавіші (10M+ завантажень npm на тиждень) - **Recharts**: `forwardRef` дозволяє батьківським компонентам прикріплювати resize observers до контейнерів графіків - **Framer Motion**: refs керують анімаціями і тригерами прокрутки - **Video.js**: передає ref у `<canvas>` для WebGL-оверлея ### Питання на співбесіді **Q:** Яка різниця між `useRef` і `createRef`? **A:** `useRef` зберігає об'єкт у стані хуків fiber і повертає те саме посилання при кожному рендері. `createRef` виділяє новий `{ current: null }` при кожному виклику. У функціональному компоненті це означає: ref скидається при кожному рендері і ніколи не вказує на корисний елемент. **Q:** Коли встановлюється `ref.current`? **A:** Під час фази фіксації (commit phase), після того як React оновив реальний DOM. До монтування - `null`. Після розмонтування React скидає його назад у `null`. Читати під час рендерингу немає сенсу. **Q:** Навіщо використовувати `useImperativeHandle` разом з `forwardRef`? **A:** Щоб відкрити контрольований API замість повного DOM-вузла. Батьківський компонент отримує лише ті методи, які ти визначив, що запобігає випадковій прямій маніпуляції DOM ззовні компонента. **Q:** Чи можуть refs спричинити витік пам'яті? **A:** Так. Якщо зберігаєш ID інтервалу, observer або слухача подій у ref і не очищаєш його в поверненні `useEffect`, ресурс залишається живим після розмонтування компонента. **Q:** Як refs поводяться в конкурентному React 18? **A:** Refs прикріплюються після того, як `commitRoot` завершує всю роботу. Нарізання часу (time slicing) не впливає на стабільність refs, бо вони живуть поза фазою мутацій fiber. Призупинений рендер з низьким пріоритетом не скидає `ref.current`. ## Приклади ### Визначення успішності автоплею ```jsx import { useRef, useEffect } from 'react'; function AutoplayVideo({ src }) { const videoRef = useRef(null); const wasPlayingRef = useRef(false); // мутабельний прапорець, рендер не потрібен useEffect(() => { const video = videoRef.current; video .play() .then(() => { wasPlayingRef.current = true; // збережено без оновлення стейту }) .catch(() => { console.log('Автоплей заблоковано браузером'); }); }, [src]); return <video ref={videoRef} src={src} muted />; } ``` Chrome блокує автоплей для відео без `muted`, тому цей атрибут обов'язковий. `wasPlayingRef` зберігає результат без повторного рендеру, бо статус автоплею не потрібно показувати у UI. ### Кастомний хук для отримання попереднього значення ```jsx import { useRef, useEffect, useState } from 'react'; function usePrevious(value) { const ref = useRef(); useEffect(() => { ref.current = value; // запускається після рендеру }, [value]); return ref.current; // повертає значення з попереднього рендеру } function Counter() { const [count, setCount] = useState(0); const prevCount = usePrevious(count); return ( <div> <p>Зараз: {count}, раніше: {prevCount}</p> <button onClick={() => setCount(count + 1)}>+</button> </div> ); } ``` `useEffect` запускається після рендеру. Тому коли компонент повертає результат, `ref.current` ще тримає значення попереднього рендеру. При наступному рендері це старе значення і повертається. Вся магія - в асиметрії часу виконання. ### Відеоплеєр із контрольованим imperative API ```jsx import { forwardRef, useRef, useImperativeHandle } from 'react'; const VideoPlayer = forwardRef(({ src }, ref) => { const videoRef = useRef(null); useImperativeHandle(ref, () => ({ play: () => videoRef.current.play(), pause: () => videoRef.current.pause(), seek: (seconds) => { videoRef.current.currentTime = seconds; }, }), []); return <video ref={videoRef} src={src} />; }); function App() { const playerRef = useRef(null); return ( <> <VideoPlayer ref={playerRef} src="demo.mp4" /> <button onClick={() => playerRef.current.play()}>Відтворити</button> <button onClick={() => playerRef.current.seek(30)}>+30с</button> </> ); } ``` Батьківський компонент має `play`, `pause` і `seek`. Він не може читати `.currentTime`, `.buffered` чи `.duration`. Ця межа і є ціллю `useImperativeHandle`.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.