Как правильно использовать 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 таймера или предыдущее значение пропса), всегда указывайте тип явно. Это убережёт вас от ошибок во время рефакторинга.
Ответов пока нет.
Войдите чтобы оставить ответ