Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Чому useImperativeHandle потрібен у React». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)`useImperativeHandle` дозволяє визначити власний публічний API дочірнього компонента, доступний батьку через ref, замість того щоб відкривати весь DOM-вузол. ```jsx const Input = forwardRef((props, ref) => { const inputRef = useRef(); useImperativeHandle(ref, () => ({ focus: () => inputRef.current.focus(), clear: () => { inputRef.current.value = ''; } })); return <input ref={inputRef} />; }); ``` **Ключове:** завжди паруй з `forwardRef`; використовуй коли батькові потрібно викликати методи-дії на дитині, а не читати її стан.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення`useImperativeHandle` - це хук React, який дозволяє визначити, що саме батьківський компонент може робити через ref на дочірній компонент, замість того щоб відкривати весь DOM-вузол. ## Теорія ### TL;DR - Думай про це як про **контрольований вихід за правила**: дані в React течуть вниз через props, але цей хук дає батьку можливість напряму викликати методи дитини - Без нього ref на дочірньому компоненті відкриває батьку доступ до всього на DOM-вузлі. З ним ти сам визначаєш публічний API - Завжди паруй з `forwardRef`, без нього параметр `ref` буде `undefined` - Використовуй коли батькові потрібно викликати дії (`focus()`, `reset()`, `scrollToTop()`), а не читати стан - Правило вибору: якщо тягнешся до `ref.current.internalState`, тобі, мабуть, потрібен `useImperativeHandle` ### Швидкий приклад ```jsx // Дитина відкриває тільки те, що потрібно батьку const Input = forwardRef((props, ref) => { const inputRef = useRef(); useImperativeHandle(ref, () => ({ focus: () => inputRef.current.focus(), clear: () => { inputRef.current.value = ''; } })); return <input ref={inputRef} />; }); // Батько може викликати тільки focus() і clear() const Parent = () => { const inputRef = useRef(); return ( <> <Input ref={inputRef} /> <button onClick={() => inputRef.current.focus()}>Focus</button> <button onClick={() => inputRef.current.clear()}>Clear</button> </> ); }; ``` Батько може викликати `focus()` і `clear()`, і більше нічого. Він не зможе випадково викликати `inputRef.current.removeChild()` або зчитати внутрішні властивості DOM-вузла. ### Навіщо це потрібно: інкапсуляція Без `useImperativeHandle` ref на дочірньому компоненті відкриває батьку прямий доступ до DOM-вузла. Всі властивості, всі методи, всі внутрішні деталі. Для звичайного `<input>` це нормально, але не для компонента зі своїм станом і логікою. `useImperativeHandle` повертає контроль тобі. Ти сам вирішуєш що відкривати. Батько отримує чистий API. Внутрішню реалізацію можна пізніше переробити, не ламаючи нічого зовнішнього. ### Як це працює зсередини `useImperativeHandle(ref, createHandle, deps)` зберігає об'єкт, який повертає `createHandle()`, у `ref.current`. Коли масив залежностей змінюється, React викликає `createHandle()` знову і замінює збережене значення. Сам ref - це просто `{ current: someValue }`. Ніякої магії, тільки контрольоване присвоєння до мутабельного контейнера. ### Коли використовувати - Батьку потрібно викликати методи-дії на дитині (`focus()`, `reset()`, `play()`, `validate()`) - Дитина керує складним внутрішнім станом, до якого батько не повинен мати прямого доступу - Обгортаєш сторонню бібліотеку (відеоплеєр, datepicker, rich text editor), де API бібліотеки за природою є imperative - Будуєш багаторазові компоненти з контрольованим, стабільним зовнішнім інтерфейсом Для передачі даних вниз достатньо props. Для перемикання видимості або анімацій - стан і CSS-переходи чистіші. ### Поширені помилки **Помилка 1: Відкриваєш забагато** ```jsx // Неправильно - батько може напряму змінювати внутрішній стан useImperativeHandle(ref, () => ({ inputRef, // сирий DOM-доступ витікає назовні internalState, // батько може зламати логіку компонента _privateMethod })); // Правильно - відкривай тільки публічний API useImperativeHandle(ref, () => ({ focus: () => inputRef.current.focus(), getValue: () => inputRef.current.value })); ``` Якщо відкрити `inputRef` напряму, батько може викликати `inputRef.current.removeChild()`. А якщо пізніше переробити внутрішню реалізацію, весь код батька, що торкається цих деталей, зламається. **Помилка 2: Пропущені залежності і застарілі замикання (stale closures)** ```jsx const [count, setCount] = useState(0); // Неправильно - getCount() завжди повертає 0 useImperativeHandle(ref, () => ({ getCount: () => count })); // немає масиву залежностей // Правильно useImperativeHandle(ref, () => ({ getCount: () => count }), [count]); ``` Метод виконується нормально, повертає значення, помилки немає. Але це застаріле замикання. Дебажити болісно, бо жодного сигналу про проблему немає, просто неправильні дані. **Помилка 3: Imperative API для стану, який має бути в props** ```jsx // Неправильно - imperative контроль модалки const Modal = forwardRef((props, ref) => { const [isOpen, setIsOpen] = useState(false); useImperativeHandle(ref, () => ({ open: () => setIsOpen(true), close: () => setIsOpen(false) })); return isOpen ? <div>Modal</div> : null; }); // Правильно - контрольований компонент const Modal = ({ isOpen, onClose }) => ( isOpen ? <div>Modal <button onClick={onClose}>X</button></div> : null ); ``` Imperative версія робить батька складнішим для розуміння. Стан розкиданий по imperative-викликах. Time-travel debugging ламається. Якщо батько має контролювати видимість, це має бути prop. **Помилка 4: Не обгорнув у forwardRef** ```jsx // Неправильно - параметр ref буде undefined const Input = (props, ref) => { useImperativeHandle(ref, () => ({ focus: () => {} })); return <input />; }; // Правильно const Input = forwardRef((props, ref) => { useImperativeHandle(ref, () => ({ focus: () => {} })); return <input />; }); ``` Без `forwardRef` функціональні компоненти не отримують `ref` другим параметром. Він буде `undefined`, і `useImperativeHandle` нічого не зможе до нього прикріпити. **Помилка 5: Умовний виклик хука** ```jsx // Неправильно - порушує правила хуків const Input = forwardRef((props, ref) => { if (props.disabled) { useImperativeHandle(ref, () => ({})); } return <input />; }); // Правильно - завжди виклик, регулюй що відкривати const Input = forwardRef((props, ref) => { useImperativeHandle(ref, () => ({ focus: props.disabled ? undefined : () => inputRef.current.focus() })); return <input />; }); ``` React відслідковує хуки по порядку виклику. Умовний виклик дає помилку «Hooks called in different order». ### Де зустрічається в реальних проектах - **React Hook Form** - відкриває `focus()`, `setValue()` на refs полів для imperative-валідації - **Material-UI** - `TextField` і `Dialog` відкривають imperative-методи через refs - **Framer Motion** - управління анімаціями через refs: play/pause/seek - **Monaco Editor** - `getValue()`, `setValue()`, `layout()` на ref редактора - **Відео та аудіо плеєри** - `play()`, `pause()`, `seek()` за природою є imperative - **Rich text editors** - `focus()`, `getContent()`, `setContent()` через refs Я бачив команди, що тягнулися до `useImperativeHandle` там, де простий callback-prop вирішив би проблему. Перед використанням перевір: чи може батько просто передати `onValidate` колбек вниз? ### Питання на співбесіді **Q:** Чому б просто не використати ref напряму на DOM-елементі? **A:** Можна, але тоді батько отримує доступ до всього на DOM-вузлі. `useImperativeHandle` дає змогу додати власну логіку до відкритих методів, наприклад аналітику або валідацію перед фокусом, і приховати внутрішні деталі. **Q:** Чи можна використати `useImperativeHandle` без `forwardRef`? **A:** Ні. Без `forwardRef` параметр `ref` в функціональному компоненті буде `undefined`. Хукові нема до чого прикріплюватись. **Q:** Якщо відкритий метод оновлює стан у дитині, чи перерендериться батько? **A:** Ні. Батько не перерендерується якщо його власний стан не змінився. Дитина перерендерується через оновлення стану всередині неї. Батько побачить нові дані тільки якщо викличе ще один метод для їх читання. **Q (рівень senior):** У формі 10 полів, кожне відкриває `validate()` через `useImperativeHandle`. Батько викликає всі 10 в циклі при сабміті. Яка проблема з продуктивністю виникне і як її вирішити? **A:** Кожен виклик `validate()` може тригерити оновлення стану в полі, що призводить до 10 окремих рендерів. Рішення: батчувати оновлення через `flushSync`, або збирати результати без змін стану (зберігати в ref, оновити стан один раз після всіх перевірок). Краще рішення: використати React Hook Form або Formik, які обробляють це без imperative refs. ## Приклади ### Кастомний input з focus і clear ```jsx import { useRef, useImperativeHandle, forwardRef } from 'react'; const CustomInput = forwardRef((props, ref) => { const inputRef = useRef(null); useImperativeHandle(ref, () => ({ focus: () => inputRef.current.focus(), clear: () => { inputRef.current.value = ''; } })); return <input ref={inputRef} {...props} />; }); export default function App() { const ref = useRef(null); return ( <div> <CustomInput ref={ref} placeholder="Введи текст" /> <button onClick={() => ref.current.focus()}>Focus</button> <button onClick={() => ref.current.clear()}>Clear</button> </div> ); } ``` У `ref.current` тільки `focus` і `clear`. Батько не може торкнутись DOM-вузла напряму, не може викликати `.remove()`, не може зчитати `.offsetHeight`. API-поверхня рівно така, яку ти обрав. ### Валідація форми з кількома полями ```jsx const FormField = forwardRef(({ name, validate }, ref) => { const [value, setValue] = useState(''); const [error, setError] = useState(''); useImperativeHandle(ref, () => ({ getValue: () => value, validate: () => { const err = validate(value); setError(err); return !err; // true якщо валідне }, reset: () => { setValue(''); setError(''); } }), [value, validate]); // deps тримають методи актуальними return ( <div> <input value={value} onChange={(e) => setValue(e.target.value)} /> {error && <span className="error">{error}</span>} </div> ); }); const Form = () => { const emailRef = useRef(); const passwordRef = useRef(); const handleSubmit = () => { const emailOk = emailRef.current.validate(); const passwordOk = passwordRef.current.validate(); if (emailOk && passwordOk) { console.log('Відправляємо...'); } }; return ( <> <FormField ref={emailRef} name="email" validate={(v) => v.includes('@') ? '' : 'Невірний email'} /> <FormField ref={passwordRef} name="password" validate={(v) => v.length >= 8 ? '' : 'Мінімум 8 символів'} /> <button onClick={handleSubmit}>Відправити</button> </> ); }; ``` Кожне поле керує своїм станом і логікою валідації. Батько викликає `validate()` і отримує boolean. Внутрішній стан нікуди не витікає. Масив залежностей `[value, validate]` тримає методи синхронізованими з поточними значеннями. ### Edge case із застарілим замиканням ```jsx const VideoPlayer = forwardRef((props, ref) => { const [isPlaying, setIsPlaying] = useState(false); const videoRef = useRef(); // Неправильно: немає deps, getStatus() завжди повертає false useImperativeHandle(ref, () => ({ play: () => { videoRef.current.play(); setIsPlaying(true); }, getStatus: () => isPlaying // захоплено застаріле значення })); // Правильно: додаємо isPlaying щоб метод завжди читав актуальне значення useImperativeHandle(ref, () => ({ play: () => { videoRef.current.play(); setIsPlaying(true); }, getStatus: () => isPlaying }), [isPlaying]); return <video ref={videoRef} src={props.src} />; }); ``` З неправильною версією `player.current.getStatus()` повертає `false` навіть після виклику `play()`. Метод виконується, повертає значення, жодної помилки. Саме тому застарілі замикання так важко помітити на code review.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.