← Назад к вопросам

Как передать ref в компонент реализованный как функция?

2.0 Middle🔥 162 комментариев
#React

Комментарии (2)

🐱
claude-haiku-4.5PrepBro AI3 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Как передать ref в компонент реализованный как функция?

Передача ref в функциональные компоненты - частый вопрос в React. По умолчанию вы не можете просто передать ref через пропсы функциональному компоненту, так как это специальное свойство, которое обрабатывает React. Для этого используется React.forwardRef.

Базовый подход с forwardRef

import { forwardRef, Ref } from "react";

interface InputProps {
  placeholder?: string;
  onChange?: (value: string) => void;
}

// Без forwardRef
const InputWithoutRef = (props: InputProps) => {
  return <input type="text" {...props} />;
};

// С forwardRef - правильно
const Input = forwardRef<HTMLInputElement, InputProps>(
  ({ placeholder, onChange }, ref) => {
    return (
      <input
        ref={ref}
        type="text"
        placeholder={placeholder}
        onChange={(e) => onChange?.(e.target.value)}
      />
    );
  }
);

Input.displayName = "Input"; // Для DevTools

// Использование
function App() {
  const inputRef = useRef<HTMLInputElement>(null);

  const handleClick = () => {
    if (inputRef.current) {
      inputRef.current.focus();
    }
  };

  return (
    <>
      <Input ref={inputRef} placeholder="Type here" />
      <button onClick={handleClick}>Focus Input</button>
    </>
  );
}

Объяснение forwardRef

forwardRef - это wrapper, который позволяет компоненту получить ref в качестве второго аргумента после props. Без него ref игнорируется.

Типы в TypeScript:

// forwardRef<RefType, PropsType>
const Component = forwardRef<HTMLDivElement, MyComponentProps>(
  (props, ref) => {
    return <div ref={ref}>{props.children}</div>;
  }
);

Передача ref к нативному элементу

Мост между пропсами и ref:

interface CardProps {
  title: string;
  children: React.ReactNode;
  className?: string;
}

const Card = forwardRef<HTMLDivElement, CardProps>(
  ({ title, children, className }, ref) => {
    return (
      <div ref={ref} className={`card ${className || ""}`}>
        <h2>{title}</h2>
        <div>{children}</div>
      </div>
    );
  }
);

Card.displayName = "Card";

// Использование
function App() {
  const cardRef = useRef<HTMLDivElement>(null);

  return (
    <Card ref={cardRef} title="My Card">
      Content here
    </Card>
  );
}

Кастомный ref с useImperativeHandle

Если вы хотите контролировать то, что доступно через ref, используйте useImperativeHandle:

interface InputHandle {
  focus: () => void;
  clear: () => void;
  getValue: () => string;
}

const Input = forwardRef<InputHandle, InputProps>(
  ({ placeholder }, ref) => {
    const inputRef = useRef<HTMLInputElement>(null);

    useImperativeHandle(ref, () => ({
      focus: () => inputRef.current?.focus(),
      clear: () => {
        if (inputRef.current) {
          inputRef.current.value = "";
        }
      },
      getValue: () => inputRef.current?.value || "",
    }));

    return <input ref={inputRef} type="text" placeholder={placeholder} />;
  }
);

Input.displayName = "Input";

// Использование
function App() {
  const inputRef = useRef<InputHandle>(null);

  return (
    <>
      <Input ref={inputRef} />
      <button onClick={() => inputRef.current?.focus()}>
        Focus
      </button>
      <button onClick={() => inputRef.current?.clear()}>
        Clear
      </button>
      <button onClick={() => console.log(inputRef.current?.getValue())}>
        Get Value
      </button>
    </>
  );
}

Типизация useImperativeHandle

// Сначала определяем интерфейс методов
interface TextEditorHandle {
  insertText: (text: string) => void;
  getText: () => string;
  clear: () => void;
}

// Потом в forwardRef указываем этот интерфейс
const TextEditor = forwardRef<TextEditorHandle, EditorProps>(
  ({ initialValue }, ref) => {
    const textRef = useRef<HTMLTextAreaElement>(null);

    useImperativeHandle(ref, () => ({
      insertText: (text) => {
        if (textRef.current) {
          textRef.current.value += text;
        }
      },
      getText: () => textRef.current?.value || "",
      clear: () => {
        if (textRef.current) {
          textRef.current.value = "";
        }
      },
    }), []);

    return <textarea ref={textRef} defaultValue={initialValue} />;
  }
);

TextEditor.displayName = "TextEditor";

Распространение ref к вложенным компонентам

const Dialog = forwardRef<HTMLDialogElement, DialogProps>(
  ({ children, title }, ref) => {
    return (
      <dialog ref={ref}>
        <h2>{title}</h2>
        {children}
      </dialog>
    );
  }
);

Dialog.displayName = "Dialog";

// Использование
function App() {
  const dialogRef = useRef<HTMLDialogElement>(null);

  return (
    <>
      <Dialog ref={dialogRef} title="Confirm">
        Are you sure?
      </Dialog>
      <button onClick={() => dialogRef.current?.showModal()}>
        Open Dialog
      </button>
    </>
  );
}

Важные замечания

  1. displayName - всегда добавляйте для DevTools
  2. Dependency array в useImperativeHandle - если пуст [], то методы никогда не пересчитываются
  3. Нельзя передать ref на функцию - только на нативные DOM элементы или компоненты с forwardRef
  4. forwardRef нарушает PureComponent и React.memo - используйте memo(Component, isEqual)
// Правильно с memo
const MemoizedInput = memo(Input);

// Если нужен и memo, и forwardRef
const Input = forwardRef<HTMLInputElement, InputProps>((props, ref) => {
  return <input ref={ref} {...props} />;
});

export default memo(Input, (prev, next) => {
  return prev.placeholder === next.placeholder;
});

Когда использовать ref

Ref нужны редко. Вот примеры когда они оправданы:

  • Управление фокусом (input.focus())
  • Срабатывание анимаций
  • Интеграция с third-party DOM библиотеками
  • Получение значений из нативных элементов

Для всего остального - используйте state и props.

Итоговый шаблон

interface CustomComponentHandle {
  method1: () => void;
  method2: (arg: string) => string;
}

const CustomComponent = forwardRef<CustomComponentHandle, Props>(
  (props, ref) => {
    const internalRef = useRef<HTMLDivElement>(null);

    useImperativeHandle(ref, () => ({
      method1: () => { /* logic */ },
      method2: (arg) => { return arg; },
    }), []);

    return <div ref={internalRef}>{props.children}</div>;
  }
);

CustomComponent.displayName = "CustomComponent";