Что такое forwardRef и для чего он нужен?

👨‍💻 Frontend Developer 🟠 Может встретиться 🎚️ Средний
#React

Краткий ответ

forwardRef — это API React, который позволяет родительским компонентам передавать ref дочерним компонентам. Он нужен, когда вы хотите получить доступ к DOM элементам или экземплярам дочерних компонентов из родительских компонентов.

Базовое использование:

// Без forwardRef - ref не будет работать
const Button = (props) => <button>{props.children}</button>;
 
// С forwardRef - ref передается дальше
const Button = React.forwardRef((props, ref) => (
  <button ref={ref}>{props.children}</button>
));

Полный ответ

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

Зачем нужен forwardRef?

Проблема без forwardRef

// Родительский компонент пытается передать ref
function Parent() {
  const buttonRef = useRef(null);
  
  return (
    // ❌ Это не будет работать - ref будет undefined
    <FancyButton ref={buttonRef}>Нажми меня</FancyButton>
  );
}
 
// Дочерний компонент не может получить ref
function FancyButton(props) {
  // props.ref не определен!
  return <button className="fancy">{props.children}</button>;
}

Решение с forwardRef

// Дочерний компонент с forwardRef
const FancyButton = React.forwardRef((props, ref) => {
  return (
    <button ref={ref} className="fancy">
      {props.children}
    </button>
  );
});
 
// Родительский компонент
function Parent() {
  const buttonRef = useRef(null);
  
  const handleClick = () => {
    // ✅ Теперь мы можем получить доступ к кнопке
    buttonRef.current.focus();
  };
  
  return (
    <>
      <FancyButton ref={buttonRef}>Нажми меня</FancyButton>
      <button onClick={handleClick}>Фокус на fancy кнопку</button>
    </>
  );
}

Распространенные сценарии использования

1. Доступ к DOM элементам в дочерних компонентах

// Кастомный компонент инпута
const CustomInput = React.forwardRef((props, ref) => {
  return (
    <div className="input-wrapper">
      <label>{props.label}</label>
      <input ref={ref} {...props} />
    </div>
  );
});
 
// Использование в форме
function LoginForm() {
  const usernameRef = useRef(null);
  const passwordRef = useRef(null);
  
  const handleSubmit = (e) => {
    e.preventDefault();
    console.log(usernameRef.current.value);
    console.log(passwordRef.current.value);
  };
  
  useEffect(() => {
    // Автофокус при монтировании
    usernameRef.current.focus();
  }, []);
  
  return (
    <form onSubmit={handleSubmit}>
      <CustomInput ref={usernameRef} label="Имя пользователя" />
      <CustomInput ref={passwordRef} label="Пароль" type="password" />
      <button type="submit">Войти</button>
    </form>
  );
}

2. Интеграция со сторонними библиотеками

// Обертка для стороннего компонента
const MapWrapper = React.forwardRef((props, ref) => {
  const mapContainerRef = useRef(null);
  
  useImperativeHandle(ref, () => ({
    // Экспортируем кастомные методы
    centerMap: (lat, lng) => {
      if (mapContainerRef.current) {
        // Вызываем метод сторонней библиотеки
        mapLibrary.setCenter(mapContainerRef.current, { lat, lng });
      }
    },
    getZoom: () => {
      return mapLibrary.getZoom(mapContainerRef.current);
    }
  }));
  
  useEffect(() => {
    // Инициализируем стороннюю библиотеку
    mapLibrary.init(mapContainerRef.current, props.options);
  }, []);
  
  return <div ref={mapContainerRef} className="map-container" />;
});
 
// Родительский компонент
function App() {
  const mapRef = useRef(null);
  
  const handleCenterMap = () => {
    mapRef.current.centerMap(40.7128, -74.0060); // Нью-Йорк
  };
  
  return (
    <div>
      <MapWrapper ref={mapRef} options={{ zoom: 10 }} />
      <button onClick={handleCenterMap}>Центрировать на Нью-Йорке</button>
    </div>
  );
}

3. Создание переиспользуемых UI компонентов

// Переиспользуемый компонент модального окна
const Modal = React.forwardRef((props, ref) => {
  const [isOpen, setIsOpen] = useState(false);
  const modalRef = useRef(null);
  
  useImperativeHandle(ref, () => ({
    open: () => setIsOpen(true),
    close: () => setIsOpen(false),
    toggle: () => setIsOpen(prev => !prev)
  }));
  
  useEffect(() => {
    if (isOpen && modalRef.current) {
      modalRef.current.focus();
    }
  }, [isOpen]);
  
  if (!isOpen) return null;
  
  return (
    <div className="modal-overlay" onClick={() => setIsOpen(false)}>
      <div 
        ref={modalRef}
        className="modal-content"
        onClick={e => e.stopPropagation()}
        tabIndex={-1}
      >
        <button 
          className="modal-close" 
          onClick={() => setIsOpen(false)}
        >
          ×
        </button>
        {props.children}
      </div>
    </div>
  );
});
 
// Использование
function App() {
  const modalRef = useRef(null);
  
  return (
    <div>
      <button onClick={() => modalRef.current.open()}>
        Открыть модалку
      </button>
      
      <Modal ref={modalRef}>
        <h2>Содержимое модалки</h2>
        <p>Это переиспользуемый компонент модального окна</p>
      </Modal>
    </div>
  );
}

Продвинутые паттерны с forwardRef

1. Обработка множественных ref

// Компонент, которому нужны и внутренний, и переданный ref
const VideoPlayer = React.forwardRef((props, forwardedRef) => {
  const internalRef = useRef(null);
  
  // Комбинируем ref'ы
  const setRefs = useCallback((node) => {
    // Устанавливаем внутренний ref
    internalRef.current = node;
    
    // Устанавливаем переданный ref
    if (forwardedRef) {
      if (typeof forwardedRef === 'function') {
        forwardedRef(node);
      } else {
        forwardedRef.current = node;
      }
    }
  }, [forwardedRef]);
  
  useEffect(() => {
    // Используем внутренний ref для логики компонента
    if (internalRef.current) {
      internalRef.current.volume = 0.5;
    }
  }, []);
  
  return <video ref={setRefs} {...props} />;
});

2. Условная передача ref

// Передаем ref разным элементам в зависимости от пропсов
const FlexibleInput = React.forwardRef((props, ref) => {
  if (props.multiline) {
    return <textarea ref={ref} {...props} />;
  }
  
  if (props.type === 'select') {
    return (
      <select ref={ref} {...props}>
        {props.options.map(opt => (
          <option key={opt.value} value={opt.value}>
            {opt.label}
          </option>
        ))}
      </select>
    );
  }
  
  return <input ref={ref} {...props} />;
});

3. HOC с forwardRef

// Компонент высшего порядка, сохраняющий ref
function withTooltip(Component) {
  const WithTooltipComponent = React.forwardRef((props, ref) => {
    const [showTooltip, setShowTooltip] = useState(false);
    
    return (
      <div className="tooltip-wrapper">
        <Component
          {...props}
          ref={ref}
          onMouseEnter={() => setShowTooltip(true)}
          onMouseLeave={() => setShowTooltip(false)}
        />
        {showTooltip && (
          <div className="tooltip">{props.tooltip}</div>
        )}
      </div>
    );
  });
  
  // Устанавливаем имя для отладки
  WithTooltipComponent.displayName = 
    `withTooltip(${Component.displayName || Component.name})`;
  
  return WithTooltipComponent;
}
 
// Использование
const ButtonWithTooltip = withTooltip(
  React.forwardRef((props, ref) => (
    <button ref={ref} {...props} />
  ))
);

TypeScript с forwardRef

Базовое использование TypeScript

interface ButtonProps {
  variant?: 'primary' | 'secondary';
  size?: 'small' | 'medium' | 'large';
  children: React.ReactNode;
  onClick?: () => void;
}
 
// Типобезопасный forwardRef компонент
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
  (props, ref) => {
    const { variant = 'primary', size = 'medium', ...rest } = props;
    
    return (
      <button
        ref={ref}
        className={`btn btn-${variant} btn-${size}`}
        {...rest}
      />
    );
  }
);
 
// Использование с TypeScript
function App() {
  const buttonRef = useRef<HTMLButtonElement>(null);
  
  const focusButton = () => {
    buttonRef.current?.focus();
  };
  
  return (
    <Button ref={buttonRef} variant="primary">
      Нажми меня
    </Button>
  );
}

Сложный пример с TypeScript

// Интерфейс кастомного ref handle
interface VideoPlayerHandle {
  play: () => void;
  pause: () => void;
  seekTo: (time: number) => void;
}
 
interface VideoPlayerProps {
  src: string;
  poster?: string;
  autoPlay?: boolean;
}
 
const VideoPlayer = React.forwardRef<VideoPlayerHandle, VideoPlayerProps>(
  (props, ref) => {
    const videoRef = useRef<HTMLVideoElement>(null);
    
    useImperativeHandle(ref, () => ({
      play: () => {
        videoRef.current?.play();
      },
      pause: () => {
        videoRef.current?.pause();
      },
      seekTo: (time: number) => {
        if (videoRef.current) {
          videoRef.current.currentTime = time;
        }
      }
    }), []);
    
    return (
      <video
        ref={videoRef}
        src={props.src}
        poster={props.poster}
        autoPlay={props.autoPlay}
      />
    );
  }
);
 
// Типобезопасное использование
function App() {
  const playerRef = useRef<VideoPlayerHandle>(null);
  
  return (
    <div>
      <VideoPlayer ref={playerRef} src="video.mp4" />
      <button onClick={() => playerRef.current?.play()}>Играть</button>
      <button onClick={() => playerRef.current?.pause()}>Пауза</button>
      <button onClick={() => playerRef.current?.seekTo(30)}>
        Перейти к 30с
      </button>
    </div>
  );
}

Оптимизация производительности

Memo с forwardRef

// Оптимизированный компонент с memo и forwardRef
const ExpensiveComponent = React.memo(
  React.forwardRef((props, ref) => {
    console.log('ExpensiveComponent отрендерен');
    
    // Тяжелые вычисления
    const result = useMemo(() => {
      return heavyCalculation(props.data);
    }, [props.data]);
    
    return (
      <div ref={ref}>
        {result}
      </div>
    );
  })
);
 
// Кастомная функция сравнения
const OptimizedComponent = React.memo(
  React.forwardRef((props, ref) => {
    return <div ref={ref}>{props.content}</div>;
  }),
  (prevProps, nextProps) => {
    // Кастомная логика сравнения
    return prevProps.content === nextProps.content;
  }
);

Частые ошибки и их решения

1. Забытое Display Name

// ❌ Плохо - нет display name
const MyComponent = React.forwardRef((props, ref) => {
  return <div ref={ref} />;
});
 
// ✅ Хорошо - с display name
const MyComponent = React.forwardRef((props, ref) => {
  return <div ref={ref} />;
});
MyComponent.displayName = 'MyComponent';

2. Неправильный тип ref

// ❌ Неправильно - ref указывает на React компонент, а не DOM
const Card = React.forwardRef((props, ref) => {
  return <AnotherComponent ref={ref} />;
});
 
// ✅ Правильно - ref указывает на DOM элемент
const Card = React.forwardRef((props, ref) => {
  return (
    <div ref={ref} className="card">
      <AnotherComponent />
    </div>
  );
});

3. Ref колбэк vs Ref объект

const FlexibleComponent = React.forwardRef((props, ref) => {
  const setRef = (node) => {
    // Обрабатываем оба типа ref
    if (ref) {
      if (typeof ref === 'function') {
        ref(node);
      } else {
        ref.current = node;
      }
    }
  };
  
  return <div ref={setRef}>{props.children}</div>;
});

Лучшие практики

  1. Используйте осмысленные display names — помогает при отладке
  2. Документируйте императивные handles — четко описывайте экспортируемые методы
  3. Типизируйте ref в TypeScript — предотвращает ошибки во время выполнения
  4. Комбинируйте с memo разумно — оптимизируйте производительность при необходимости
  5. Сохраняйте простоту передаваемых ref — избегайте сложной логики с ref
  6. Тестируйте функциональность ref — убедитесь, что ref работают как ожидается

ForwardRef — важный инструмент для создания переиспользуемых React компонентов, которым нужно предоставлять доступ к своим внутренним DOM элементам или императивным API родительским компонентам.


Хотите больше статей для подготовки к собеседованиям? Подписывайтесь на EasyAdvice, добавляйте сайт в закладки и совершенствуйтесь каждый день 💪