Сайт использует сookies для хранения данных. Продолжая использовать сайт, вы даёте согласие на работу с этими файлами.

ОК
💻
Технологии
Опубликовано:
14.09.2026
Обновлено:
14.09.2026

ComponentPropsWithoutRef vs ComponentPropsWithRef в React 19: как правильно типизировать пропсы и ref

Тимофей Ищенко

Коротко: Разбираем разницу между ComponentPropsWithoutRef и ComponentPropsWithRef в React 19. Правильная типизация пропсов, ref без forwardRef и лучшие практики.

В экосистеме React и TypeScript грамотная типизация базовых пропсов компонентов всегда была залогом надежного API дизайн-систем и библиотек. С релизом React 19 парадигма работы со ссылками на DOM-узлы и инстансы компонентов претерпела ключевые изменения: вспомогательная функция forwardRef объявлена устаревшей (deprecated), а ref стал стандартным пропом функциональных компонентов.

Эти изменения напрямую повлияли на выбор утилитных типов в пакете @types/react. Разберем, как устроены ComponentPropsWithoutRef, ComponentPropsWithRef и базовый ComponentProps, в каких сценариях каждый из них необходим и как выстраивать надежные контракты компонентов в React 19.


Что изменилось в React 19: прощание с forwardRef

Исторически функциональные компоненты React не могли принимать ref в качестве обычного аргумента props. Для проброса ссылки разработчикам приходилось оборачивать компонент в вызов React.forwardRef((props, ref) => ...). Это усложняло сигнатуру, мешало чистой передаче дженериков и требовало отдельного внимания при типизации через TypeScript.

В React 19 механизм унифицирован: функциональные компоненты принимают ref напрямую внутри объекта props, точно так же, как className, onClick или children.

// React 19: ref — стандартный проп в сигнатуре функции
interface ButtonProps {
  variant?: 'primary' | 'secondary';
  children: React.ReactNode;
  ref?: React.Ref<HTMLButtonElement>;
}

export function Button({ variant = 'primary', children, ref, ...rest }: ButtonProps) {
  return (
    <button ref={ref} className={`btn btn-${variant}`} {...rest}>
      {children}
    </button>
  );
}

Хотя forwardRef сохраняет обратную совместимость для постепенной миграции проектов, в режиме разработки он выводит предупреждение. Новый подход упростил код, но вопрос строгости контракта API остался актуальным: разрешает ли компонент передачу ref или намеренно скрывает доступ к внутреннему DOM-узлу?


Анатомия утилитных типов @types/react

Пакет @types/react предоставляет три родственные утилиты для извлечения типов пропсов HTML-элементов и кастомных компонентов: ComponentProps<T>, ComponentPropsWithRef<T> и ComponentPropsWithoutRef<T>.

ComponentPropsWithRef<T>

Тип ComponentPropsWithRef<T> извлекает все допустимые пропсы для переданного тега или компонента T, включая корректно типизированный проп ref.

import type { ComponentPropsWithRef } from 'react';

// Тип содержит все атрибуты <button>, а также ref: React.Ref<HTMLButtonElement>
type NativeButtonProps = ComponentPropsWithRef<'button'>;

Если T — это нативный HTML-тег (например, 'input', 'div'), утилита автоматически подставит правильный интерфейс DOM-элемента (HTMLInputElement, HTMLDivElement) в тип ref.

ComponentPropsWithoutRef<T>

Утилита ComponentPropsWithoutRef<T> берет полный набор пропсов элемента или компонента, но принудительно исключает свойство ref с помощью оператора Omit.

import type { ComponentPropsWithoutRef } from 'react';

// Содержит все атрибуты <button>, но исключает проп ref
type ButtonWithoutRefProps = ComponentPropsWithoutRef<'button'>;

ComponentProps<T>

В актуальных версиях @types/react тип ComponentProps<T> является алиасом для ComponentPropsWithRef<T>. Он по умолчанию включает в себя ref. Однако в строгих кодовых базах и библиотеках компонентов явное указание WithRef или WithoutRef считается более надежным решением, так как оно открыто декларирует архитектурное намерение автора.


Практические сценарии выбора паттерна

Выбор между утилитами определяется тем, передает ли компонент ref дальше в DOM-дерево или изолирует внутреннюю разметку.

Сценарий 1. Компонент-обертка без поддержки ref (ComponentPropsWithoutRef)

Если компонент инкапсулирует сложную логику, состоит из нескольких вложенных узлов или намеренно не должен отдавать доступ к своему внутреннему DOM-элементу, необходимо использовать ComponentPropsWithoutRef.

import type { ComponentPropsWithoutRef } from 'react';

interface CardProps extends ComponentPropsWithoutRef<'div'> {
  title: string;
  isElevated?: boolean;
}

export function Card({ title, isElevated, children, className, ...rest }: CardProps) {
  return (
    <div className={`card ${isElevated ? 'shadow' : ''} ${className ?? ''}`} {...rest}>
      <header className="card-header">
        <h3>{title}</h3>
      </header>
      <div className="card-body">{children}</div>
    </div>
  );
}

Почему это важно: если внешний потребитель попытается передать <Card ref={cardRef} />, TypeScript немедленно выдаст ошибку на этапе компиляции. Если бы здесь использовался ComponentPropsWithRef, проп ref был бы валидным с точки зрения типов, попал бы в ...rest, но не связался бы с реальным узлом, создав трудноуловимый баг неработающей ссылки.

Сценарий 2. Базовый элемент дизайн-системы с пробросом ref (ComponentPropsWithRef)

В UI-kit компоненты атомарного уровня (кнопки, поля ввода, чекбоксы) обязаны поддерживать ref для интеграции с библиотеками форм (React Hook Form), вычисления координат (поп-апы, тултипы) и программного управления фокусом.

В React 19 типизация такого компонента выглядит лаконично:

import type { ComponentPropsWithRef } from 'react';

interface InputProps extends ComponentPropsWithRef<'input'> {
  hasError?: boolean;
  label?: string;
}

export function TextInput({ hasError, label, id, className, ref, ...rest }: InputProps) {
  return (
    <div className="input-group">
      {label && <label htmlFor={id}>{label}</label>}
      <input
        id={id}
        ref={ref}
        className={`input-field ${hasError ? 'input-error' : ''} ${className ?? ''}`}
        {...rest}
      />
    </div>
  );
}

Тип ComponentPropsWithRef<'input'> гарантирует, что ref принимает именно React.Ref<HTMLInputElement>, что исключает передачу несовместимого рефа.

Сценарий 3. Извлечение пропсов из сторонних кастомных компонентов

Обе утилиты работают не только с именами HTML-тегов, но и с типами функций через оператор typeof. Это необходимо при создании HOC, композиционных оберток или адаптеров.

import type { ComponentPropsWithoutRef, ComponentPropsWithRef } from 'react';
import { ThirdPartyModal } from 'third-party-library';

// Извлекаем пропсы без ref, если оборачиваем сторонний компонент в изолирующий провайдер
type SafeModalProps = ComponentPropsWithoutRef<typeof ThirdPartyModal>;

// Извлекаем пропсы вместе с ref для сохранения полного контракта
type FullModalProps = ComponentPropsWithRef<typeof ThirdPartyModal>;

Типичные ошибки и подводные камни

  1. Неконтролируемый спред (...rest) при использовании ComponentPropsWithRef. Если в интерфейсе объявлен ComponentPropsWithRef, но разработчик забыл явно деструктурировать ref и просто делает <div {...rest}> внутри сложной структуры, ссылка может прикрепиться не к тому элементу, который ожидает потребитель, либо перезаписаться.

  2. Затирание типов обработчиков событий. При расширении интерфейсов иногда некорректно переопределяют обработчики:

    // Ошибка: сужение типа может вызвать конфликт с нативным интерфейсом
    interface BadProps extends ComponentPropsWithoutRef<'button'> {
      onClick: () => void; // Нарушает сигнатуру (event: React.MouseEvent<HTMLButtonElement>) => void
    }
    

    Если требуется кастомная сигнатура, нативное свойство следует предварительно исключить через Omit<ComponentPropsWithoutRef<'button'>, 'onClick'>.

  3. Смешение паттернов React 18 и React 19. Использование React.ElementRef<typeof Component> вместе с forwardRef в кодовой базе на React 19 создает избыточный бойлерплейт. Достаточно типизировать props через ComponentPropsWithRef<T> без оборачивания функции.


Сравнительная шпаргалка: что выбирать в React 19

Критерий ComponentPropsWithoutRef<T> ComponentPropsWithRef<T> ComponentProps<T>
Наличие пропа ref Исключен (Omit) Включен Включен (алиас WithRef)
Основное назначение Компоненты-контейнеры, составные блоки, скрывающие DOM Атомарные UI-элементы, инпуты, кнопки дизайн-системы Быстрое прототипирование пропсов
Защита от silent fail Высокая (ошибка компиляции при попытке передать ref) Требует явного проброса ref в целевой узел Требует явного проброса ref в целевой узел
Совместимость с React 19 Полная Полная Полная
Рекомендация для UI-kit Для составных виджетов и карточек Для базовых элементов с пробросом ref Для вспомогательных служебных типов

Часто задаваемые вопросы (FAQ)

Зачем нужен ComponentPropsWithoutRef, если в React 19 ref — это обычный проп?

ComponentPropsWithoutRef исключает ref из контракта компонента. Если компонент не пробрасывает ссылку на внутренний DOM-элемент, запрет на передачу ref защищает разработчиков от ситуации, когда ссылка передана в JSX, но фактически не привязана к узлу и остается неинициализированной во время выполнения.

Являются ли ComponentProps<T> и ComponentPropsWithRef<T> взаимозаменяемыми в React 19?

С технической точки зрения в современных типах @types/react тип ComponentProps<T> ссылается на ComponentPropsWithRef<T>. Однако использование ComponentPropsWithRef делает архитектурное намерение кода более очевидным при командной разработке и код-ревью.

Что произойдет, если продолжать использовать forwardRef в React 19?

Компоненты с forwardRef продолжат компилироваться и работать благодаря слою обратной совместимости. Однако React 19 выводит предупреждение об устаревании в консоль разработки, а сама обертка добавляет лишний уровень вложенности и усложняет типы TypeScript.

Как правильно типизировать полиморфный компонент с учетом ref в React 19?

Для полиморфных компонентов формируют дженерик-тип: type PolymorphicProps<E extends React.ElementType> = ComponentPropsWithRef<E> & { as?: E }. Это позволяет TypeScript динамически подставлять правильный тип ref в зависимости от переданного тега (например, HTMLAnchorElement для as="a" или HTMLButtonElement для as="button").

Нужно ли типизировать ref отдельно, если используется ComponentPropsWithRef?

Нет. Тип ComponentPropsWithRef<'tag'> уже содержит свойство ref с типом React.Ref<ElementType>. Ручное добавление ref?: ... в кастомный интерфейс является избыточным дублированием.


Заключение

Переход на React 19 существенно упростил работу со ссылками в компонентах, избавив фронтенд-разработку от конструкции forwardRef. Тем не менее, строгая типизация пропсов остается фундаментом надежной архитектуры.

Используйте ComponentPropsWithRef, когда компонент выступает тонкой оберткой над DOM-узлом и делегирует управление ссылкой внешнему коду. Применяйте ComponentPropsWithoutRef, если компонент инкапсулирует внутреннюю разметку и не предназначен для прямой манипуляции через DOM-ссылки. Явное разграничение этих сценариев защищает кодовую базу от скрытых ошибок и формирует предсказуемый интерфейс дизайн-системы.

Источники

Это авторская статья, основанная на личном опыте и субъективном взгляде автора. Заметили ошибку или битую ссылку? Сообщите нам: info@codesrc.ru - мы оперативно исправим. Спасибо, что помогаете делать блог лучше.
Следите за нами в соцсетях:

Читайте также