Как правильно использовать useRef и createRef в React с TypeScript: типизация рефов
Правильная типизация useRef в React с TypeScript
Проблема в вашем коде — отсутствие явного указания типа для рефа. Когда вы пишете useRef(null), TypeScript выводит тип MutableRefObject, из-за чего current становится null, и вы не можете вызвать методы DOM-элемента.
Вот правильное решение для вашего случая с полем ввода:
import React, { useRef, useEffect } from 'react';
const MyComponent: React.FC = () => {
// Явно указываем тип HTMLInputElement и начальное значение null
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
// Теперь TypeScript знает, что current — это HTMLInputElement | null
// Опциональная цепочка (?.) безопасно вызывает focus()
inputRef.current?.focus();
}, []);
return <input ref={inputRef} type="text" />;
};
Почему это работает?
- Тип-параметр:
<HTMLInputElement>сообщает TypeScript, какой именно DOM-элемент будет храниться в рефе. - Начальное значение
null: Реф инициализируется до монтирования компонента, поэтому начальное значение —null. TypeScript понимает это и автоматически делает типReact.RefObject<HTMLInputElement>(илиMutableRefObjectв зависимости от версии React). - Опциональная цепочка:
inputRef.current?.focus()безопасно вызывает метод, только еслиcurrentне равенnull.
Другие распространённые случаи
1. Для разных HTML-элементов:
const divRef = useRef<HTMLDivElement>(null);
const buttonRef = useRef<HTMLButtonElement>(null);
const videoRef = useRef<HTMLVideoElement>(null);
2. Для React-компонентов (forwardRef):
Если ваш компонент обёрнут в forwardRef, нужно типизировать и сам компонент, и реф:
// Компонент с forwardRef
interface FancyInputProps {
placeholder?: string;
}
const FancyInput = React.forwardRef<HTMLInputElement, FancyInputProps>(
(props, ref) => {
return <input ref={ref} placeholder={props.placeholder} />;
}
);
// Использование
const ParentComponent: React.FC = () => {
const inputRef = useRef<HTMLInputElement>(null);
const handleClick = () => {
inputRef.current?.focus();
};
return (
<>
<FancyInput ref={inputRef} placeholder="Введите текст" />
<button onClick={handleClick}>Фокус</button>
</>
);
};
3. Для хранения произвольных значений (не DOM):
Если реф не привязан к DOM-элементу, а хранит, например, таймер или счётчик:
const TimerComponent: React.FC = () => {
// Для произвольных значений используем MutableRefObject
const timerRef = useRef<number | null>(null);
const countRef = useRef<number>(0); // Начальное значение не null
const startTimer = () => {
timerRef.current = window.setInterval(() => {
countRef.current += 1;
console.log(countRef.current);
}, 1000);
};
const stopTimer = () => {
if (timerRef.current !== null) {
clearInterval(timerRef.current);
timerRef.current = null;
}
};
return (
<div>
<p>Счётчик: {countRef.current}</p>
<button onClick={startTimer}>Старт</button>
<button onClick={stopTimer}>Стоп</button>
</div>
);
};
Важные замечания
- Разница между useRef и createRef: В функциональных компонентах всегда используйте
useRef.createRefпредназначен для классовых компонентов. - Тип для ref в JSX: Когда вы передаёте
ref={inputRef}, TypeScript автоматически проверяет, что тип рефа совместим с элементом. Например,useRef<HTMLInputElement>нельзя передать в<div>— будет ошибка. - Callback refs: Иногда удобнее использовать колбэк-рефы:
const [height, setHeight] = useState(0);
const measuredRef = useCallback((node: HTMLDivElement | null) => {
if (node !== null) {
setHeight(node.getBoundingClientRect().height);
}
}, []);
return <div ref={measuredRef}>Измеряемый элемент</div>;
Этот подход часто используется для измерения размеров элементов или интеграции с библиотеками, работающими с DOM напрямую.
<<Альтернативный подход: использование createRef с классами и обобщённые типы
Хотя в современных React-приложениях принято использовать функциональные компоненты с хуками, в некоторых проектах всё ещё встречаются классовые компоненты. Вот как правильно типизировать рефы в них:
import React, { createRef, Component } from 'react';
interface MyClassComponentProps {
initialValue?: string;
}
class MyClassComponent extends Component<MyClassComponentProps> {
// createRef автоматически выводит тип, если указать generic
private inputRef = createRef<HTMLInputElement>();
componentDidMount() {
// Безопасный доступ через current?.focus()
this.inputRef.current?.focus();
}
render() {
return <input ref={this.inputRef} defaultValue={this.props.initialValue} />;
}
}
Обобщённый хелпер для callback-рефов:
Если вы часто используете колбэк-рефы, можно создать вспомогательную функцию для типизации:
function createCallbackRef<T extends HTMLElement>(callback: (element: T) => void) {
return (node: T | null) => {
if (node !== null) {
callback(node);
}
};
}
// Использование:
const MyComponent: React.FC = () => {
const handleRef = createCallbackRef<HTMLInputElement>((input) => {
console.log('Ширина инпута:', input.offsetWidth);
});
return <input ref={handleRef} />;
};
Этот подход особенно полезен, когда нужно выполнять какие-то действия с элементом сразу после его монтирования (например, подписка на события или интеграция с jQuery).
Совет: Если вам нужно хранить в рефе значение, которое может меняться, но не вызывает ререндер (например, ID таймера или предыдущее значение пропса), всегда указывайте тип явно. Это убережёт вас от ошибок во время рефакторинга.