Refs, forwardRef & useImperativeHandle

Expose a small, deliberate imperative API from a component without leaking its internals.

Most of the time data flows down through props and events flow up through callbacks — that is the declarative model and you should stay in it. But some things are genuinely imperative: focusing an input, scrolling to an element, playing a video, opening a dialog. For those, refs are the escape hatch.

Passing a ref to your own component

A ref given to a custom component does not automatically reach a DOM node inside it. In React 19 you can accept ref as a regular prop; before 19 you wrapped the component in forwardRef. Both let a parent point a ref at your inner element.

// React 19: ref is just a prop
function TextInput({ ref, ...props }) {
  return <input ref={ref} {...props} />;
}

Expose intent, not implementation

useImperativeHandle lets you decide exactly what the parent's ref can do. Instead of handing over the raw DOM node, you publish a tiny, named API — focus(), clear(), scrollIntoView() — and keep everything else private. This is the imperative equivalent of a well-designed public interface.

When to reach for it

  • Reusable form fields that need a focus() the parent can call after validation.
  • Media or canvas components with play() / pause().
  • Design-system inputs where you want the DOM node hidden but a couple of actions exposed.

Example

Example · javascript
import { useRef, useImperativeHandle } from 'react';

// A field that exposes a deliberate imperative API: focus + clear.
function SearchField({ ref, placeholder }) {
  const inputRef = useRef(null);

  useImperativeHandle(ref, () => ({
    focus() {
      inputRef.current?.focus();
    },
    clear() {
      if (inputRef.current) inputRef.current.value = '';
    },
  }), []);

  return <input ref={inputRef} placeholder={placeholder} />;
}

function Toolbar() {
  const searchRef = useRef(null);
  return (
    <div>
      <SearchField ref={searchRef} placeholder="Search..." />
      <button onClick={() => searchRef.current.focus()}>Focus</button>
      <button onClick={() => searchRef.current.clear()}>Clear</button>
    </div>
  );
}

When to use it

  • A design system's Input component forwards its ref so parent forms can call .focus() on the raw input element for error-recovery UX.
  • A video player component wraps the <video> element and uses forwardRef so a parent MediaController can call .play() and .pause() directly.
  • A custom Dialog component uses useImperativeHandle to expose only open() and close() methods through its ref, hiding all internal animation state from callers.

More examples

forwardRef to expose DOM node

Wraps the inner input in forwardRef so a parent can call .focus() on it, even though the element is inside a child.

Example · jsx
import { forwardRef } from 'react';

const TextInput = forwardRef(function TextInput({ label, ...props }, ref) {
  return (
    <label>
      {label}
      <input ref={ref} {...props} />
    </label>
  );
});

// Parent
import { useRef, useEffect } from 'react';
function Form() {
  const inputRef = useRef(null);
  useEffect(() => inputRef.current?.focus(), []);
  return <TextInput ref={inputRef} label="Name" />;
}

useImperativeHandle to restrict API

Uses useImperativeHandle to expose only focus and clear, hiding the raw DOM node from the parent completely.

Example · jsx
import { forwardRef, useImperativeHandle, useRef } from 'react';

const FancyInput = forwardRef(function FancyInput(props, ref) {
  const innerRef = useRef(null);

  useImperativeHandle(ref, () => ({
    focus: () => innerRef.current?.focus(),
    clear: () => { if (innerRef.current) innerRef.current.value = ''; },
  }));

  return <input ref={innerRef} {...props} />;
});

// Parent can call: inputRef.current.focus() or inputRef.current.clear()
// But cannot access innerRef or any other internals

forwardRef with TypeScript

Adds generic type parameters to forwardRef so the ref type and props type are both checked at compile time.

Example · tsx
import { forwardRef, InputHTMLAttributes } from 'react';

interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
  label: string;
}

const Input = forwardRef<HTMLInputElement, InputProps>(
  function Input({ label, ...rest }, ref) {
    return (
      <label>
        {label}
        <input ref={ref} {...rest} />
      </label>
    );
  }
);

export default Input;

Discussion

  • Be the first to comment on this lesson.