C
React/Advanced/Lesson 21

forwardRef and useImperativeHandle

25 min·theory
This chapter
2/2
TypeScript

forwardRef and useImperativeHandle

💡 Why should you learn this?

🎯 This is an advanced technique that every library developer must know.
💼 It is essential when building uncontrolled components.
This is a concept that frequently appears in technical interviews at large companies.
🏢 실무에서는
When building a UI library, the parent component may need to directly control a child's input. For example, when imperative operations such as 'setting focus' or 'resetting a value' are required, you use forwardRef together with useImperativeHandle. This is an essential skill for library developers.

What is forwardRef?

forwardRef and useImperativeHandle

The problem: refs cannot be passed as props

By default, ref is a special prop and is not forwarded to child components.

forwardRef

Connects the ref passed by the parent to a DOM element or instance inside the child component.

useImperativeHandle

Used together with forwardRef to explicitly define the interface exposed to the parent.

Use cases

  • Controlling focus() on a custom Input component
  • Controlling scroll position
  • Form library integration (React Hook Form's Controller)
  • Building reusable component libraries

React 19 change

Starting with React 19, you can receive ref as a regular prop without forwardRef.

💻 Implementing forwardRef
import { forwardRef, useRef, useImperativeHandle, useState } from 'react';

// 1. Basic forwardRef: Passing DOM ref
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
  label?: string;
  error?: string;
}

// forwardRef<ref type, props type>
const CustomInput = forwardRef<HTMLInputElement, InputProps>(
  ({ label, error, ...props }, ref) => {
    return (
      <div className="flex flex-col gap-1">
        {label && <label className="text-sm font-medium">{"Email"}</label>}
        {/* Connect ref to the actual input DOM */}
        <input
          ref={ref}
          className={`border rounded px-3 py-2 ${
            error ? 'border-red-500' : 'border-gray-300'
          }`}
          {...props}
        />
        {error && <p className="text-red-500 text-xs">{error}</p>}
      </div>
    );
  }
);

CustomInput.displayName = 'CustomInput'; // Display name in DevTools

// Access input DOM directly via ref from parent component
function LoginForm() {
  const emailRef = useRef<HTMLInputElement>(null);
  const passwordRef = useRef<HTMLInputElement>(null);

  const focusEmail = () => emailRef.current?.focus(); // Control focus externally

  return (
    <form>
      <CustomInput ref={emailRef} label="Email" type="email" />
      <CustomInput ref={passwordRef} label="Password" type="password" />
      <button type="button" onClick={focusEmail}>{"Focus on Email"}</button>
    </form>
  );
}

// 2. useImperativeHandle: Customizing exposed API
interface VideoPlayerHandle {
  play: () => void;
  pause: () => void;
  seek: (seconds: number) => void;
  getCurrentTime: () => number;
}

interface VideoPlayerProps {
  src: string;
  autoPlay?: boolean;
}

const VideoPlayer = forwardRef<VideoPlayerHandle, VideoPlayerProps>(
  ({ src, autoPlay = false }, ref) => {
    const videoRef = useRef<HTMLVideoElement>(null);

    // Define methods to expose to parent (expose only selected APIs, not the entire DOM)
    useImperativeHandle(ref, () => ({
      play: () => videoRef.current?.play(),
      pause: () => videoRef.current?.pause(),
      seek: (seconds) => {
        if (videoRef.current) videoRef.current.currentTime = seconds;
      },
      getCurrentTime: () => videoRef.current?.currentTime ?? 0,
    }), []); // dependency array

    return (
      <video
        ref={videoRef}
        src={src}
        autoPlay={autoPlay}
        controls
        className="w-full rounded"
      />
    );
  }
);

// Parent: Can only use exposed APIs (cannot access entire DOM)
function VideoPage() {
  const playerRef = useRef<VideoPlayerHandle>(null);

  return (
    <div>
      <VideoPlayer ref={playerRef} src="/video.mp4" />
      <div className="flex gap-2 mt-2">
        <button onClick={() => playerRef.current?.play()}>{"Play"}</button>
        <button onClick={() => playerRef.current?.pause()}>{"Pause"}</button>
        <button onClick={() => playerRef.current?.seek(30)}>{"Seek to 30 seconds"}</button>
      </div>
    </div>
  );
}

// 3. React 19 method (forwardRef not needed)
// function NewInput({ ref, ...props }: InputProps & { ref?: React.Ref<HTMLInputElement> }) {
//   return <input ref={ref} {...props} />;
// }

💡 Practical tips

  • displayName: Set displayName on a forwardRef component to show its name in React DevTools
  • useImperativeHandle recommended: Expose only the methods you need instead of the entire DOM, for better encapsulation
  • React 19: ref can be passed as a regular prop; forwardRef is no longer needed (backward compatibility maintained)
  • React Hook Form: forwardRef is used internally inside the Controller component
  • TypeScript: Use the forwardRef<RefType, PropsType> generic for type-safe usage

⚛️ React Pattern — forwardRef and useImperativeHandle

Learn step by step, with code, how forwardRef and useImperativeHandle are used in React.
1 🧩 1. Scenarios where forwardRef and useImperativeHandle are needed
Situations where these features are required.
2 💻 2. Writing the code
Basic usage of forwardRef and useImperativeHandle.
3 🎨 3. Rendering result
What the user sees on screen.
4 💡 4. Real-world tips
Common pitfalls and best practices.

🎮 forwardRef and useImperativeHandle — Step-by-step Understanding

Click each step to read the content, then use the 'Got it' button to track your progress.
🖥️ Result — rendered React component
✏️ React 코드 수정하기 (클릭해서 열기)
⚛️ React 18 + Babel Standalone — see the result first, then edit the code yourself.

Check your understanding

When do you need forwardRef?
💡 Functional components cannot receive a ref by default. Wrapping the component with forwardRef allows the child component to receive the parent's ref and attach it to an internal DOM element. This is commonly used in UI library components.
forwardRef + useImperativeHandle - React