Refs в React (useRef, createRef, forwardRef)
Что такое Refs?
Refs (ссылки) — это способ получить прямой доступ к DOM-элементам или React-компонентам из кода.
Refs используются когда нужно:
- Управлять фокусом, выделением текста
- Запускать анимации
- Интегрироваться со сторонними библиотеками
- Измерять размеры элементов
useRef
useRef — хук для создания рефов в функциональных компонентах.
Доступ к 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}>Focus 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(); // Cleanup
}, []);
return (
<div>
<p>Count: {count}</p>
<button onClick={start}>Start</button>
<button onClick={stop}>Stop</button>
</div>
);
}
useRef vs useState
// useState - вызывает перерисовку
const [value, setValue] = useState(0);
// useRef - НЕ вызывает перерисовку
const valueRef = useRef(0);
valueRef.current = 1; // Не вызовет render
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}>Focus</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}>Focus</button>
</div>
);
}
useImperativeHandle
useImperativeHandle позволяет настроить, что именно будет доступно родителю через реф.
Без 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}>Focus</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' }}>
Measure me
</div>
<p>Width: {dimensions.width}px</p>
<p>Height: {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>Current: {count}</p>
<p>Previous: {prevCount}</p>
<button onClick={() => setCount(count + 1)}>+</button>
</div>
);
}
Callback Refs
Альтернативный способ работы с рефами — callback функция:
function Component() {
const [height, setHeight] = useState(0);
const measuredRef = useCallback(node => {
if (node !== null) {
setHeight(node.getBoundingClientRect().height);
}
}, []);
return (
<div>
<div ref={measuredRef}>
<p>This div's height is {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}>Dialog</div>;
}
// Хорошо
function Dialog() {
const [isOpen, setIsOpen] = useState(false);
return isOpen ? <div>Dialog</div> : null;
}
Не храните данные, которые влияют на рендер
// Плохо
function Component() {
const dataRef = useRef([]);
function addItem(item) {
dataRef.current.push(item); // Компонент не перерисуется!
}
return <div>{dataRef.current.length} items</div>;
}
// Хорошо
function Component() {
const [data, setData] = useState([]);
function addItem(item) {
setData(prev => [...prev, item]); // Перерисовка произойдёт
}
return <div>{data.length} items</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}>Hello</div>;
}
// Правильно
function Component() {
const ref = useRef(null);
useEffect(() => {
console.log(ref.current); // Элемент доступен
}, []);
return <div ref={ref}>Hello</div>;
}
Создание нового ref на каждом рендере
// Неправильно
function Component() {
const ref = createRef(); // Новый ref на каждом рендере!
return <div ref={ref}>Hello</div>;
}
// Правильно
function Component() {
const ref = useRef(null); // Один и тот же ref
return <div ref={ref}>Hello</div>;
}
Вывод
Refs в React:
useRef— для функциональных компонентовcreateRef— для классовых компонентовforwardRef— для проброса рефа через компонентuseImperativeHandle— для контроля доступа к рефу- Не вызывают перерисовку при изменении
- Используйте когда декларативный подход невозможен
- Избегайте для того, что можно сделать через state
На собеседовании:
Важно уметь:
- Объяснить, что такое refs и когда их использовать
- Показать разницу между useRef и useState
- Объяснить, для чего нужны forwardRef и useImperativeHandle
- Привести примеры правильного использования рефов
- Рассказать, когда НЕ нужно использовать refs