Avatar ·

Как правильно использовать useRef и createRef в React с TypeScript: типизация рефов

Всем привет! Я новичок в React и TypeScript, и у меня возникла проблема с использованием рефов. Я пытаюсь получить доступ к DOM-элементу (например, к полю ввода) через useRef, но TypeScript постоянно выдает ошибки типов. Вот мой код: ```typescript import React, { useRef, useEffect } from 'react'; const MyComponent: React.FC = () => { const inputRef = useRef(null); // Ошибка: Argument of type 'null' is not assignable to parameter of type 'HTMLInputElement' useEffect(() => { inputRef.current?.focus(); // Ошибка: Property 'focus' does not exist on type 'never' }, []); return ; }; ``` Я пробовал разные варианты, но TypeScript ругается то на инициализацию `useRef(null)`, то на попытку вызвать `focus()`. Пробовал использовать `createRef` — та же история. В JavaScript такой код работал без проблем, а в TypeScript я застрял. Подскажите, как правильно типизировать рефы в React + TypeScript? Нужно ли указывать тип элемента явно? Или есть какой-то специальный синтаксис? Буду благодарен за подробное объяснение с примерами для разных случаев: для HTML-элементов, для React-компонентов и для хранения произвольных значений. <<>>

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