Refs у React (useRef, createref, forwardref)
Що таке Refs?
Refs (посилання) — це спосіб отримати прямий доступ до DOM-елементів або React-компонентів з коду.
Refs використовуються, коли вам потрібно:
- Керувати фокусом, вибором тексту
- Запускати анімації
- Інтегруватися з бібліотеками сторонніх розробників
- Вимірювати розміри елементів
useRef
useRef — це хук для створення refs у функціональних компонентх.
Доступ до DOM-елементів
import { useRef } from 'react';
function TextInput() {
const inputRef = useRef(null);
function handleClick() {
// Отримати прямий доступ до input
inputRef.current.focus();
}
return (
<div>
<input ref={inputRef} type="text" />
<button onClick={handleClick}>Фокус на Input</button>
</div>
);
}Зберігання змінного значення
useRef можна використовувати для зберігання будь-якого значення, яке не викликає повторний рендер:
function Timer() {
const [count, setCount] = useState(0);
const intervalRef = useRef(null);
function start() {
if (intervalRef.current) return;
intervalRef.current = setInterval(() => {
setCount(c => c + 1);
}, 1000);
}
function stop() {
if (intervalRef.current) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
}
useEffect(() => {
return () => stop(); // Очистка
}, []);
return (
<div>
<p>Кількість: {count}</p>
<button onClick={start}>Почати</button>
<button onClick={stop}>Зупинити</button>
</div>
);
}useRef vs useState
// useState - викликає повторний рендер
const [value, setValue] = useState(0);
// useRef - НЕ викликає повторний рендер
const valueRef = useRef(0);
valueRef.current = 1; // Не викличе рендерcreateRef
createRef використовується в класових компонентх:
class TextInput extends React.Component {
constructor(props) {
super(props);
this.inputRef = React.createRef();
}
handleClick = () => {
this.inputRef.current.focus();
};
render() {
return (
<div>
<input ref={this.inputRef} type="text" />
<button onClick={this.handleClick}>Фокус</button>
</div>
);
}
}Важливо:
Не використовуйте createRef у функціональних компонентх! Нове посилання буде створено при кожному рендері. Використовуйте useRef.
forwardRef
forwardRef дозволяє передавати посилання через компонент до його дитини.
Проблема
function CustomInput(props) {
return <input {...props} />;
}
// Не працює! ref не передається
function Parent() {
const inputRef = useRef(null);
return <CustomInput ref={inputRef} />; // Помилка!
}Рішення
const CustomInput = forwardRef((props, ref) => {
return <input ref={ref} {...props} />;
});
function Parent() {
const inputRef = useRef(null);
function handleClick() {
inputRef.current.focus();
}
return (
<div>
<CustomInput ref={inputRef} />
<button onClick={handleClick}>Фокус</button>
</div>
);
}useImperativeHandle
useImperativeHandle дозволяє налаштувати, що доступно батьківському компоненту через ref.
Без useImperativeHandle
const CustomInput = forwardRef((props, ref) => {
return <input ref={ref} />;
});
// Батьківський компонент отримує доступ до всього DOM inputЗ useImperativeHandle
import { forwardRef, useImperativeHandle, useRef } from 'react';
const CustomInput = forwardRef((props, ref) => {
const inputRef = useRef(null);
useImperativeHandle(ref, () => ({
// Відкриваємо лише ці методи
focus: () => {
inputRef.current.focus();
},
scrollIntoView: () => {
inputRef.current.scrollIntoView();
}
// value, blur та інші методи недоступні
}));
return <input ref={inputRef} {...props} />;
});
function Parent() {
const inputRef = useRef(null);
function handleClick() {
inputRef.current.focus(); // Працює
// inputRef.current.value; // undefined!
}
return (
<div>
<CustomInput ref={inputRef} />
<button onClick={handleClick}>Фокус</button>
</div>
);
}Практичні приклади
Автофокус при монтуванні
function AutoFocusInput() {
const inputRef = useRef(null);
useEffect(() => {
inputRef.current.focus();
}, []);
return <input ref={inputRef} />;
}Вимірювання розмірів елемента
function MeasureComponent() {
const divRef = useRef(null);
const [dimensions, setDimensions] = useState({});
useEffect(() => {
if (divRef.current) {
const { width, height } = divRef.current.getBoundingClientRect();
setDimensions({ width, height });
}
}, []);
return (
<div>
<div ref={divRef} style={{ padding: 20, backgroundColor: 'lightblue' }}>
Виміряй мене
</div>
<p>Ширина: {dimensions.width}px</p>
<p>Висота: {dimensions.height}px</p>
</div>
);
}Інтеграція з бібліотекою сторонніх розробників
function VideoPlayer({ src }) {
const videoRef = useRef(null);
useEffect(() => {
// Ініціалізація плеєра з бібліотекою сторонніх розробників
const player = new ThirdPartyPlayer(videoRef.current);
player.load(src);
return () => {
player.destroy();
};
}, [src]);
return <video ref={videoRef} />;
}Попереднє значення
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}</p>
<p>Попереднє: {prevCount}</p>
<button onClick={() => setCount(count + 1)}>+</button>
</div>
);
}Callback Refs
Альтернативний спосіб роботи з refs за допомогою колбек-функції:
function Component() {
const [height, setHeight] = useState(0);
const measuredRef = useCallback(node => {
if (node !== null) {
setHeight(node.getBoundingClientRect().height);
}
}, []);
return (
<div>
<div ref={measuredRef}>
<p>Висота цього div'а {height}px</p>
</div>
</div>
);
}Коли НЕ використовувати Refs
Не використовуйте для того, що можна зробити декларативно
// Погано
function Dialog() {
const dialogRef = useRef(null);
function open() {
dialogRef.current.style.display = 'block';
}
function close() {
dialogRef.current.style.display = 'none';
}
return <div ref={dialogRef}>Діалог</div>;
}
// Добре
function Dialog() {
const [isOpen, setIsOpen] = useState(false);
return isOpen ? <div>Діалог</div> : null;
}Не зберігайте дані, які впливають на рендеринг
// Погано
function Component() {
const dataRef = useRef([]);
function addItem(item) {
dataRef.current.push(item); // Компонент не перерендериться!
}
return <div>{dataRef.current.length} елементів</div>;
}
// Добре
function Component() {
const [data, setData] = useState([]);
function addItem(item) {
setData(prev => [...prev, item]); // Відбудеться повторний рендер
}
return <div>{data.length} елементів</div>;
}Типізація TypeScript
import { useRef, forwardRef, useImperativeHandle } from 'react';
// useRef з DOM-елементом
function Component() {
const inputRef = useRef<HTMLInputElement>(null);
function handleClick() {
inputRef.current?.focus();
}
return <input ref={inputRef} />;
}
// forwardRef
interface Props {
placeholder?: string;
}
const CustomInput = forwardRef<HTMLInputElement, Props>((props, ref) => {
return <input ref={ref} {...props} />;
});
// useImperativeHandle
interface CustomInputHandle {
focus: () => void;
reset: () => void;
}
const CustomInput = forwardRef<CustomInputHandle, Props>((props, ref) => {
const inputRef = useRef<HTMLInputElement>(null);
useImperativeHandle(ref, () => ({
focus() {
inputRef.current?.focus();
},
reset() {
if (inputRef.current) {
inputRef.current.value = '';
}
}
}));
return <input ref={inputRef} {...props} />;
});Загальні помилки
Доступ до ref.current до монтування
// Неправильно
function Component() {
const ref = useRef(null);
console.log(ref.current); // null! Елемент ще не створено
return <div ref={ref}>Привіт</div>;
}
// Правильно
function Component() {
const ref = useRef(null);
useEffect(() => {
console.log(ref.current); // Елемент доступний
}, []);
return <div ref={ref}>Привіт</div>;
}Створення нового ref при кожному рендері
// Неправильно
function Component() {
const ref = createRef(); // Новий ref при кожному рендері!
return <div ref={ref}>Привіт</div>;
}
// Правильно
function Component() {
const ref = useRef(null); // Той самий ref
return <div ref={ref}>Привіт</div>;
}Висновок
Refs у React:
useRef— для функціональних компонентівcreateRef— для класових компонентівforwardRef— для передачі ref через компонентuseImperativeHandle— для контролю доступу до ref- Не викликають повторний рендер при зміні
- Використовуйте, коли декларативний підхід неможливий
- Уникайте для того, що можна зробити через state
На співбесідах:
Важливо вміти:
- Пояснити, що таке refs і коли їх використовувати
- Показати різницю між useRef та useState
- Пояснити, для чого потрібні forwardRef та useImperativeHandle
- Навести приклади правильного використання refs
- Описати, коли НЕ використовувати refs
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.