| |

React 47 ⚛️ Exposing Imperative Handles with useImperativeHandle

⏱️ Estimated read: 20 min | |

React’s model is declarative: the parent describes what it wants, and the child renders it. The parent does not reach into the child and call the methods; it passes the props, and the child reacts. But some operations are inherently imperative. The parent needs to focus the input inside the child, scroll the list inside the child, play the video inside the child, or trigger the animation inside the child. The element is not in the parent’s JSX; it is inside the child’s template. The ref cannot reach it directly, because the ref points to the child component, not to the element inside. useImperativeHandle is the hook that lets the child expose a controlled, imperative API to the parent, without giving the parent access to the raw DOM node.

The hook is the counterpart to forwardRef. The forwardRef forwards the ref from the parent to the child, and the useImperativeHandle defines what the ref points to. Without the useImperativeHandle, the forwarded ref points to the child’s DOM node, and the parent can call any DOM method on it. With the useImperativeHandle, the ref points to the object the child constructs, and the parent can call only the methods the child exposes. The object is the imperative handle, and the methods are the public API of the child. The child decides what the parent can do, and the raw DOM node stays private.

The trade-off is the abstraction. The imperative handle is the escape hatch from the declarative model, and the escape hatch should be narrow. A child that exposes the focus, the scrollTo, and the play methods is the reasonable API. A child that exposes the raw DOM node is not, because the parent can then do anything, and the child’s internal structure is no longer encapsulated. The useImperativeHandle is the way to expose the minimum, and the minimum is the set of the operations the parent genuinely needs.

This chapter covers three areas. First, why useImperativeHandle exists — the imperative-operation problem and the encapsulation problem. Second, how to use it — the forwardRef combination, the useImperativeHandle signature, the dependency array, and the TypeScript typing. Third, the patterns and the pitfalls — the focus, the scroll, the media, and the form APIs, and the cases where the declarative approach is the better choice. The chapter ends with a complete example session, a quick reference, best practices, common pitfalls, real-world examples, and diagrams showing the imperative handle flow.

Key point: useImperativeHandle lets a child component expose a controlled imperative API to the parent through a ref. It is used with forwardRef (or with the ref-as-a-prop in React 19), and it defines what the ref points to. The ref points to the object the child constructs, not the raw DOM node, so the child controls the API. The dependency array determines when the handle is recreated, and the handle should expose the minimum the parent needs.


Why useImperativeHandle exists

The imperative-operation problem. React’s declarative model is the default: the parent passes the props, and the child renders. But some operations are not expressible as the props. The focus is the imperative command: the input is focused when the user clicks a button, and the button is the sibling, not the parent. The scrollTo is the imperative command: the list is scrolled to the bottom when the new message arrives, and the list is inside the child. The play is the imperative command: the video is played when the user presses the play button. These operations need the reference to the element, and the element is inside the child. The useImperativeHandle is the mechanism that exposes the reference.

The encapsulation problem. The forwardRef alone forwards the ref to the child’s DOM node, and the parent can call any DOM method on it. The parent can inputRef.current.remove() or inputRef.current.innerHTML = '...', which bypasses the child’s logic and breaks the encapsulation. The useImperativeHandle restricts the API: the ref points to the object the child constructs, and the parent can call only the methods the child exposes. The child decides what the parent can do, and the raw DOM node stays private.

The reuse problem. A reusable component like the FancyInput is used in many places, and the parents need to focus it. The component cannot expose the raw input, because the parents would depend on the internal structure. The useImperativeHandle exposes the focus method, and the parents call it. If the internal structure changes — the input is wrapped, the ref is moved — the focus method still works, and the parents are unaffected.

The third-party-integration problem. A component that wraps a third-party library often needs to expose the library’s imperative API. The map component wraps the Leaflet, and the parent needs to call the setView or the addMarker. The useImperativeHandle exposes the library’s methods, and the parent uses them. The child encapsulates the library, and the parent sees the controlled API.

The declarative-alternative problem. The imperative handle is the escape hatch, and the declarative approach is usually better. The focus can be done with the autoFocus prop or with the key and the useEffect. The scrollTo can be done with the scrollIntoView on the element. The play can be done with the playing prop and the useEffect. The imperative handle is for the cases where the declarative approach is awkward or impossible, not the default.

The trade-off. The imperative handle is the escape hatch from the declarative model, and the escape hatch has the cost. The parent depends on the child’s imperative API, and the API is not the part of the declarative data flow. The API can be the source of the subtle bugs: the ref is null before the mount, the handle is stale after the dependency change, the call happens at the wrong time. The trade-off is between the precision of the imperative control and the simplicity of the declarative flow.


a. The basic useImperativeHandle

The useImperativeHandle is used inside a component that receives a ref. The forwardRef wraps the component, and the useImperativeHandle defines the handle.

import { forwardRef, useImperativeHandle, useRef } from 'react';

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

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

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

export default FancyInput;

The forwardRef forwards the parent’s ref to the FancyInput. The useImperativeHandle receives the forwarded ref and the factory function, and it sets the ref’s current to the object the factory returns. The object has the focus and the clear methods, and the parent can call them.

The parent uses the component with the ref:

function Parent() {
  const fancyRef = useRef(null);

  return (
    <>
      <FancyInput ref={fancyRef} />
      <button onClick={() => fancyRef.current.focus()}>Focus</button>
      <button onClick={() => fancyRef.current.clear()}>Clear</button>
    </>
  );
}

The fancyRef.current is the object with the focus and the clear methods. The parent calls them, and the child performs the imperative operation on the internal input.

The dependency array. The useImperativeHandle accepts the third argument: the dependency array. The handle is recreated when the dependencies change.

useImperativeHandle(ref, () => ({
  focus() {
    inputRef.current.focus();
  },
}), []);

The empty array means the handle is created once, on the mount. The handle’s methods close over the inputRef, which is stable, so the handle does not need to be recreated. If the handle’s methods depend on the the props or the state, the dependencies must include them:

useImperativeHandle(ref, () => ({
  focus() {
    inputRef.current.focus();
    if (props.selectAll) {
      inputRef.current.select();
    }
  },
}), [props.selectAll]);

The handle is recreated when the selectAll changes, so the parent’s ref always points to the handle with the current selectAll.

The React 19 ref-as-a-prop. In React 19, the forwardRef is no longer required. The ref is a regular prop, and the component receives it directly.

function FancyInput({ ref, ...props }) {
  const inputRef = useRef(null);

  useImperativeHandle(ref, () => ({
    focus() {
      inputRef.current.focus();
    },
  }));

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

The ref is destructured from the props, and the useImperativeHandle uses it. The parent uses the component the same way:

<FancyInput ref={fancyRef} />

The React 19 approach removes the forwardRef wrapper, and the ref is the regular prop. The React 18 and the earlier versions require the forwardRef.


b. The TypeScript typing

The imperative handle has a type, and the forwardRef and the useImperativeHandle are typed accordingly.

import { forwardRef, useImperativeHandle, useRef } from 'react';

export interface FancyInputHandle {
  focus: () => void;
  clear: () => void;
}

const FancyInput = forwardRef<FancyInputHandle, { placeholder?: string }>(
  function FancyInput(props, ref) {
    const inputRef = useRef<HTMLInputElement>(null);

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

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

export default FancyInput;

The forwardRef<FancyInputHandle, { placeholder?: string }> declares the handle type and the props type. The useImperativeHandle is typed by the FancyInputHandle, and the factory returns the matching object. The parent’s useRef<FancyInputHandle>(null) is the matching type.

The parent:

function Parent() {
  const fancyRef = useRef<FancyInputHandle>(null);

  return (
    <>
      <FancyInput ref={fancyRef} />
      <button onClick={() => fancyRef.current?.focus()}>Focus</button>
    </>
  );
}

The fancyRef.current?.focus() uses the optional chaining, because the ref may be null before the mount.

The React 19 typing. In React 19, the ref is the regular prop, and the typing is the prop type:

interface FancyInputProps {
  ref?: React.Ref<FancyInputHandle>;
  placeholder?: string;
}

function FancyInput({ ref, ...props }: FancyInputProps) {
  // ...
}

The ref is the optional prop of the React.Ref type, and the useImperativeHandle is typed the same way.


c. The patterns

The focus pattern. The most common use of the useImperativeHandle is the focus. The child wraps the input, and the parent focuses it.

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

  useImperativeHandle(ref, () => ({
    focus: () => inputRef.current?.focus(),
    select: () => inputRef.current?.select(),
  }));

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

The handle exposes the focus and the select, and the parent calls them.

The scroll pattern. The child wraps the list, and the parent scrolls it.

const MessageList = forwardRef(function MessageList({ messages }, ref) {
  const listRef = useRef(null);

  useImperativeHandle(ref, () => ({
    scrollToBottom: () => {
      if (listRef.current) {
        listRef.current.scrollTop = listRef.current.scrollHeight;
      }
    },
    scrollToTop: () => {
      if (listRef.current) {
        listRef.current.scrollTop = 0;
      }
    },
  }));

  return (
    <div ref={listRef} style={{ height: 200, overflow: 'auto' }}>
      {messages.map((m) => (
        <p key={m.id}>{m.text}</p>
      ))}
    </div>
  );
});

The parent calls the scrollToBottom when the new message arrives:

function Chat({ messages }) {
  const listRef = useRef(null);

  useEffect(() => {
    listRef.current?.scrollToBottom();
  }, [messages]);

  return <MessageList ref={listRef} messages={messages} />;
}

The media pattern. The child wraps the video, and the parent controls the playback.

const VideoPlayer = forwardRef(function VideoPlayer({ src }, ref) {
  const videoRef = useRef(null);

  useImperativeHandle(ref, () => ({
    play: () => videoRef.current?.play(),
    pause: () => videoRef.current?.pause(),
    seek: (time) => {
      if (videoRef.current) {
        videoRef.current.currentTime = time;
      }
    },
  }));

  return <video ref={videoRef} src={src} />;
});

The parent calls the play, the pause, and the seek.

The form pattern. The child wraps the form, and the parent submits and resets it.

const Form = forwardRef(function Form({ children }, ref) {
  const formRef = useRef(null);

  useImperativeHandle(ref, () => ({
    submit: () => formRef.current?.requestSubmit(),
    reset: () => formRef.current?.reset(),
    focusFirst: () => {
      const first = formRef.current?.querySelector('input, select, textarea');
      first?.focus();
    },
  }));

  return <form ref={formRef}>{children}</form>;
});

The parent calls the submit, the reset, and the focusFirst.

The third-party pattern. The child wraps the third-party library, and the parent calls the library’s methods.

const Map = forwardRef(function Map({ center, zoom }, ref) {
  const mapRef = useRef(null);
  const mapInstanceRef = useRef(null);

  useEffect(() => {
    mapInstanceRef.current = new LeafletMap(mapRef.current, { center, zoom });
    return () => mapInstanceRef.current?.remove();
  }, []);

  useImperativeHandle(ref, () => ({
    setView: (center, zoom) => mapInstanceRef.current?.setView(center, zoom),
    addMarker: (latlng) => mapInstanceRef.current?.addMarker(latlng),
  }));

  return <div ref={mapRef} style={{ height: 400 }} />;
});

The parent calls the setView and the addMarker, and the child encapsulates the Leaflet.

The declarative alternative. The imperative handle is not always the right choice. The autoFocus prop focuses the input on the mount, and the useEffect focuses it on the condition. The scrollIntoView scrolls the element, and the useEffect triggers it. The declarative approach is the simpler and the more React-idiomatic, and the imperative handle is for the cases where the declarative approach is awkward.


Complete Example Session

// ============================================
// PART 1: THE BASIC IMPERATIVE HANDLE
// ============================================

import { forwardRef, useImperativeHandle, useRef } from 'react';

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

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

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

function Parent() {
  const fancyRef = useRef(null);

  return (
    <>
      <FancyInput ref={fancyRef} />
      <button onClick={() => fancyRef.current.focus()}>Focus</button>
      <button onClick={() => fancyRef.current.clear()}>Clear</button>
    </>
  );
}


// ============================================
// PART 2: THE DEPENDENCY ARRAY
// ============================================

const FancyInput = forwardRef(function FancyInput({ selectAll }, ref) {
  const inputRef = useRef(null);

  useImperativeHandle(ref, () => ({
    focus() {
      inputRef.current.focus();
      if (selectAll) {
        inputRef.current.select();
      }
    },
  }), [selectAll]);

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


// ============================================
// PART 3: THE REACT 19 REF-AS-A-PROP
// ============================================

function FancyInput({ ref, ...props }) {
  const inputRef = useRef(null);

  useImperativeHandle(ref, () => ({
    focus() {
      inputRef.current.focus();
    },
  }));

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


// ============================================
// PART 4: THE TYPESCRIPT TYPING
// ============================================

export interface FancyInputHandle {
  focus: () => void;
  clear: () => void;
}

const FancyInput = forwardRef<FancyInputHandle, { placeholder?: string }>(
  function FancyInput(props, ref) {
    const inputRef = useRef<HTMLInputElement>(null);

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

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


// ============================================
// PART 5: THE FOCUS PATTERN
// ============================================

const SearchInput = forwardRef(function SearchInput(props, ref) {
  const inputRef = useRef(null);

  useImperativeHandle(ref, () => ({
    focus: () => inputRef.current?.focus(),
    select: () => inputRef.current?.select(),
  }));

  return <input type="search" ref={inputRef} {...props} />;
});


// ============================================
// PART 6: THE SCROLL PATTERN
// ============================================

const MessageList = forwardRef(function MessageList({ messages }, ref) {
  const listRef = useRef(null);

  useImperativeHandle(ref, () => ({
    scrollToBottom: () => {
      if (listRef.current) {
        listRef.current.scrollTop = listRef.current.scrollHeight;
      }
    },
  }));

  return (
    <div ref={listRef} style={{ height: 200, overflow: 'auto' }}>
      {messages.map((m) => (
        <p key={m.id}>{m.text}</p>
      ))}
    </div>
  );
});

function Chat({ messages }) {
  const listRef = useRef(null);

  useEffect(() => {
    listRef.current?.scrollToBottom();
  }, [messages]);

  return <MessageList ref={listRef} messages={messages} />;
}


// ============================================
// PART 7: THE MEDIA PATTERN
// ============================================

const VideoPlayer = forwardRef(function VideoPlayer({ src }, ref) {
  const videoRef = useRef(null);

  useImperativeHandle(ref, () => ({
    play: () => videoRef.current?.play(),
    pause: () => videoRef.current?.pause(),
    seek: (time) => {
      if (videoRef.current) {
        videoRef.current.currentTime = time;
      }
    },
  }));

  return <video ref={videoRef} src={src} />;
});


// ============================================
// PART 8: THE FORM PATTERN
// ============================================

const Form = forwardRef(function Form({ children }, ref) {
  const formRef = useRef(null);

  useImperativeHandle(ref, () => ({
    submit: () => formRef.current?.requestSubmit(),
    reset: () => formRef.current?.reset(),
    focusFirst: () => {
      const first = formRef.current?.querySelector('input, select, textarea');
      first?.focus();
    },
  }));

  return <form ref={formRef}>{children}</form>;
});


// ============================================
// PART 9: THE THIRD-PARTY PATTERN
// ============================================

const Map = forwardRef(function Map({ center, zoom }, ref) {
  const mapRef = useRef(null);
  const mapInstanceRef = useRef(null);

  useEffect(() => {
    mapInstanceRef.current = new LeafletMap(mapRef.current, { center, zoom });
    return () => mapInstanceRef.current?.remove();
  }, []);

  useImperativeHandle(ref, () => ({
    setView: (center, zoom) => mapInstanceRef.current?.setView(center, zoom),
    addMarker: (latlng) => mapInstanceRef.current?.addMarker(latlng),
  }));

  return <div ref={mapRef} style={{ height: 400 }} />;
});


// ============================================
// PART 10: THE DECLARATIVE ALTERNATIVE
// ============================================

// The imperative handle is not always the right choice.

// THE FOCUS:
// The autoFocus prop focuses on the mount.
function SearchInput({ autoFocus }) {
  return <input type="search" autoFocus={autoFocus} />;
}

// THE SCROLL:
// The scrollIntoView on the element.
function Chat({ messages }) {
  const endRef = useRef(null);

  useEffect(() => {
    endRef.current?.scrollIntoView({ behavior: 'smooth' });
  }, [messages]);

  return (
    <div>
      {messages.map((m) => (
        <p key={m.id}>{m.text}</p>
      ))}
      <div ref={endRef} />
    </div>
  );
}

// The declarative approach is the simpler and the more
// React-idiomatic. The imperative handle is for the
// cases where the declarative approach is awkward.

The ten parts show the basic imperative handle, the dependency array, the React 19 ref-as-a-prop, the TypeScript typing, the focus pattern, the scroll pattern, the media pattern, the form pattern, the third-party pattern, and the declarative alternative.


Quick Reference

The useImperativeHandle Signature

ArgumentMeaning
refThe forwarded ref
createHandleThe factory returning the handle
depsThe dependency array (optional)

The forwardRef Combination

VersionThe approach
React 18 and earlierforwardRef + useImperativeHandle
React 19The ref as a regular prop + useImperativeHandle
The handle typeinterface Handle { ... }
The ref typeuseRef<Handle>(null)

The Handle Methods

MethodPurpose
focus()Focus the input
clear()Clear the input
select()Select the text
scrollToBottom()Scroll the list
play() / pause()The media control
submit() / reset()The form control

The Patterns

PatternThe handle
The focusinputRef.current?.focus()
The scrolllistRef.current.scrollTop = ...
The mediavideoRef.current?.play()
The formformRef.current?.requestSubmit()
The third-partyThe library’s methods

The Declarative Alternatives

The imperativeThe declarative
The focusThe autoFocus prop
The scrollThe scrollIntoView on the element
The playThe playing prop + the useEffect
The submitThe onSubmit handler

Best Practices

✅ Do This:

// Use the forwardRef with the useImperativeHandle
const FancyInput = forwardRef(function FancyInput(props, ref) {
  useImperativeHandle(ref, () => ({ focus: () => ... }));                 // ✅
});
// Use the dependency array for the handle
useImperativeHandle(ref, () => ({ ... }), [selectAll]);                   // ✅
// Expose the minimum API
useImperativeHandle(ref, () => ({ focus, select }));                      // ✅
// Use the TypeScript handle interface
export interface FancyInputHandle { focus: () => void; }                  // ✅
// Use the optional chaining for the ref
fancyRef.current?.focus();                                                // ✅
// Prefer the declarative approach when it works
<input autoFocus={autoFocus} />                                           // ✅

❌ Don’t Do This:

// Don't expose the raw DOM node
useImperativeHandle(ref, () => inputRef.current);  // the parent can do anything // ❌
// Don't forget the dependency array when the handle depends on the props
useImperativeHandle(ref, () => ({ ... }));  // the stale closure               // ❌
// Don't use the imperative handle for the declarative cases
useImperativeHandle(ref, () => ({ setValue: (v) => setValue(v) }));           // ❌
// Don't call the ref methods before the mount
fancyRef.current.focus();  // the ref is null                                // ❌
// Don't forget the forwardRef in the React 18 and earlier
function FancyInput(props, ref) { ... }  // the ref is undefined              // ❌
// Don't overuse the imperative handle
// The declarative approach is usually better                              // ❌

Common Pitfalls

PitfallWhy It HappensFix
The ref is nullThe call before the mountUse the optional chaining
The handle is staleThe missing dependencyAdd the dependency
The forwardRef is missingThe React 18 and earlierAdd the forwardRef
The raw DOM is exposedThe handle returns the nodeReturn the object with the methods
The methods are not typedThe missing interfaceAdd the handle interface
The imperative handle is overusedThe declarative alternativeUse the declarative approach
The dependency causes the recreationThe object dependencyUse the primitive or the stable

Real-World Examples

1. The Basic Handle

const FancyInput = forwardRef(function FancyInput(props, ref) {
  const inputRef = useRef(null);
  useImperativeHandle(ref, () => ({ focus: () => inputRef.current.focus() }));
  return <input ref={inputRef} {...props} />;
});

2. The Dependency Array

useImperativeHandle(ref, () => ({ ... }), [selectAll]);

3. The React 19 Prop

function FancyInput({ ref, ...props }) {
  useImperativeHandle(ref, () => ({ ... }));
  return <input {...props} />;
}

4. The Typing

export interface FancyInputHandle {
  focus: () => void;
}

5. The Focus

useImperativeHandle(ref, () => ({ focus: () => inputRef.current?.focus() }));

6. The Scroll

useImperativeHandle(ref, () => ({
  scrollToBottom: () => { listRef.current.scrollTop = listRef.current.scrollHeight; },
}));

7. The Media

useImperativeHandle(ref, () => ({
  play: () => videoRef.current?.play(),
  pause: () => videoRef.current?.pause(),
}));

8. The Form

useImperativeHandle(ref, () => ({
  submit: () => formRef.current?.requestSubmit(),
  reset: () => formRef.current?.reset(),
}));

9. The Third-Party

useImperativeHandle(ref, () => ({
  setView: (center, zoom) => mapInstanceRef.current?.setView(center, zoom),
}));

10. The Declarative Alternative

<input autoFocus={autoFocus} />

Visual

The Imperative Handle Flow

┌──────────────────────────────────────────────────────────────┐
│  THE IMPERATIVE HANDLE FLOW                                  │
│                                                              │
│  The Parent:                                                 │
│  ┌──────────────────────────────────────────────────────┐    │
│  │  const fancyRef = useRef(null);                      │    │
│  │                                                       │    │
│  │  <FancyInput ref={fancyRef} />                       │    │
│  │  <button onClick={() => fancyRef.current.focus()}>   │    │
│  └──────────────────────────────────────────────────────┘    │
│    │                                                         │
│    │  The ref is forwarded to the child.                     │
│    ▼                                                         │
│  The Child:                                                  │
│  ┌──────────────────────────────────────────────────────┐    │
│  │  const FancyInput = forwardRef(function (props, ref) {│   │
│  │    const inputRef = useRef(null);                    │    │
│  │                                                       │    │
│  │    useImperativeHandle(ref, () => ({                 │    │
│  │      focus: () => inputRef.current.focus(),          │    │
│  │      clear: () => inputRef.current.value = '',       │    │
│  │    }));                                              │    │
│  │                                                       │    │
│  │    return <input ref={inputRef} />;                  │    │
│  │  });                                                 │    │
│  └──────────────────────────────────────────────────────┘    │
│    │                                                         │
│    │  The ref points to the object with the focus and clear. │
│    ▼                                                         │
│  The Parent calls the fancyRef.current.focus(), and the      │
│  child focuses the internal input.                           │
│                                                              │
└──────────────────────────────────────────────────────────────┘

The Encapsulation

┌──────────────────────────────────────────────────────────────┐
│  THE ENCAPSULATION                                           │
│                                                              │
│  WITHOUT the useImperativeHandle:                            │
│  ┌──────────────────────────────────────────────────────┐    │
│  │  const FancyInput = forwardRef((props, ref) => {     │    │
│  │    return <input ref={ref} />;                       │    │
│  │  });                                                 │    │
│  │                                                      │    │
│  │  The parent's ref points to the RAW <input>.         │    │
│  │  The parent can:                                     │    │
│  │    fancyRef.current.focus()                          │    │
│  │    fancyRef.current.remove()   ← the break           │    │
│  │    fancyRef.current.innerHTML = '...'  ← the break   │    │
│  └──────────────────────────────────────────────────────┘    │
│                                                              │
│  WITH the useImperativeHandle:                               │
│  ┌──────────────────────────────────────────────────────┐    │
│  │  const FancyInput = forwardRef((props, ref) => {     │    │
│  │    const inputRef = useRef(null);                    │    │
│  │    useImperativeHandle(ref, () => ({                 │    │
│  │      focus: () => inputRef.current.focus(),          │    │
│  │      clear: () => inputRef.current.value = '',       │    │
│  │    }));                                              │    │
│  │    return <input ref={inputRef} />;                  │    │
│  │  });                                                 │    │
│  │                                                      │    │
│  │  The parent's ref points to the OBJECT.              │    │
│  │  The parent can:                                     │    │
│  │    fancyRef.current.focus()                          │    │
│  │    fancyRef.current.clear()                          │    │
│  │  The parent CANNOT:                                  │    │
│  │    fancyRef.current.remove()                         │    │
│  └──────────────────────────────────────────────────────┘    │
│                                                              │
└──────────────────────────────────────────────────────────────┘

The Dependency Array

┌──────────────────────────────────────────────────────────────┐
│  THE DEPENDENCY ARRAY                                        │
│                                                              │
│  const FancyInput = forwardRef(function FancyInput(          │
│    { selectAll }, ref                                        │
│  ) {                                                         │
│    const inputRef = useRef(null);                            │
│                                                              │
│    useImperativeHandle(ref, () => ({                         │
│      focus() {                                               │
│        inputRef.current.focus();                             │
│        if (selectAll) {                                      │
│          inputRef.current.select();                          │
│        }                                                     │
│      },                                                      │
│    }), [selectAll]);                                         │
│                                                              │
│    return <input ref={inputRef} />;                          │
│  });                                                         │
│                                                              │
│  ┌──────────────────────────────────────────────────────┐    │
│  │  The handle is recreated when the selectAll changes. │    │
│  │  The parent's ref always points to the handle with   │    │
│  │  the current selectAll.                              │    │
│  │                                                       │    │
│  │  Without the dependency, the handle closes over the  │    │
│  │  initial selectAll, and the focus method uses the    │    │
│  │  stale value.                                        │    │
│  └──────────────────────────────────────────────────────┘    │
│                                                              │
└──────────────────────────────────────────────────────────────┘

The Declarative Alternative

┌──────────────────────────────────────────────────────────────┐
│  THE DECLARATIVE ALTERNATIVE                                 │
│                                                              │
│  THE IMPERATIVE (the useImperativeHandle):                   │
│  ┌──────────────────────────────────────────────────────┐    │
│  │  const SearchInput = forwardRef((props, ref) => {    │    │
│  │    const inputRef = useRef(null);                    │    │
│  │    useImperativeHandle(ref, () => ({                 │    │
│  │      focus: () => inputRef.current.focus(),          │    │
│  │    }));                                              │    │
│  │    return <input ref={inputRef} />;                  │    │
│  │  });                                                 │    │
│  │                                                       │    │
│  │  The parent: <SearchInput ref={ref} />               │    │
│  │              ref.current.focus()                     │    │
│  └──────────────────────────────────────────────────────┘    │
│                                                              │
│  THE DECLARATIVE (the autoFocus):                            │
│  ┌──────────────────────────────────────────────────────┐    │
│  │  function SearchInput({ autoFocus }) {               │    │
│  │    return <input autoFocus={autoFocus} />;           │    │
│  │  }                                                   │    │
│  │                                                       │    │
│  │  The parent: <SearchInput autoFocus />               │    │
│  └──────────────────────────────────────────────────────┘    │
│                                                              │
│  The declarative is the simpler and the more                 │
│  React-idiomatic. The imperative is for the cases where      │
│  the declarative is awkward.                                 │
│                                                              │
└──────────────────────────────────────────────────────────────┘

Summary

ItemValue
useImperativeHandleExposes the imperative API through the ref
The combinationforwardRef (the React 18) or the ref prop (the React 19)
The factoryReturns the handle object
The dependency arrayThe recreation of the handle
The handleThe object with the methods
The refThe parent’s reference to the handle
The typingThe interface Handle { ... }
The declarative alternativeThe autoFocus, the scrollIntoView

Key takeaways:

  • useImperativeHandle exposes a controlled imperative API through the ref. The child uses the hook to define what the ref points to, and the parent calls the methods. The ref points to the object the child constructs, not the raw DOM node, so the child controls the API .
  • The hook is used with the forwardRef. The forwardRef forwards the parent’s ref to the child, and the useImperativeHandle defines the handle. In React 19, the forwardRef is no longer required, and the ref is the regular prop .
  • The dependency array controls the recreation. The handle is recreated when the dependencies change. The empty array creates the handle once, and the dependencies create it when the props or the state change. The missing dependencies cause the stale closures .
  • The handle should expose the minimum. The focus, the scrollTo, and the play are the reasonable API. The raw DOM node is not, because the parent can then do anything and the child’s encapsulation is broken .
  • The imperative handle is the escape hatch. The declarative approach is usually better: the autoFocus for the focus, the scrollIntoView for the scroll, the playing prop for the media. The imperative handle is for the cases where the declarative approach is awkward or impossible .
  • The TypeScript typing has the handle interface. The export interface FancyInputHandle declares the methods, and the forwardRef<FancyInputHandle, Props> and the useRef<FancyInputHandle>(null) use the type. The optional chaining handles the null before the mount .
  • The third-party integration is the common use case. The component wraps the library and exposes the library’s methods through the handle. The parent uses the controlled API, and the child encapsulates the library’s complexity .
  • The ref may be null before the mount. The fancyRef.current?.focus() uses the optional chaining. The call before the mount is the error, and the optional chaining is the guard .

Remember: useImperativeHandle is the hook that exposes a controlled imperative API through the ref. It is used with the forwardRef (or the ref prop in React 19), and it defines what the ref points to. The child controls the API, and the raw DOM node stays private. The dependency array controls the recreation, and the handle should expose the minimum. The declarative approach is usually better, and the imperative handle is the escape hatch for the cases where the declarative is awkward. The result is the controlled imperative access, which is the foundation of the reusable component APIs.



Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!