Home → Call a child component's method from the parent in React 19: ref as a prop + useImperativeHandle

Call a child component's method from the parent in React 19: ref as a prop + useImperativeHandle

By · Node.js & JavaScript developer
Published June 12, 2023 · Updated October 1, 2026

Normally data flows down in React: the parent passes props, and the child renders. Sometimes the parent needs to tell a child to do something: focus an input, clear a field, scroll a list, play a video. That's what refs plus useImperativeHandle are for.

Before React 19 this required wrapping the child in forwardRef. In React 19, ref is just a prop on function components, and the React docs say forwardRef "will be deprecated in a future release". Everything below was tested with [email protected]: the main example in headless Chromium 153, Firefox 155 and WebKit 26.6 (via the official Playwright Docker image), and the rest with jsdom on Node 24 and 26. The TypeScript version was checked with tsc --strict (TypeScript 7.0.2, @types/[email protected]).

The child: ref as a prop

import { useImperativeHandle, useRef } from 'react';

// React 19: `ref` is just a prop. No forwardRef needed.
export function SearchBox({ ref, placeholder }) {
  const input = useRef(null);

  // Expose a small, deliberate API instead of the raw <input> element.
  useImperativeHandle(ref, () => ({
    focus() {
      input.current.focus();
    },
    clear() {
      input.current.value = '';
    },
  }), []);

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

Two things to notice:

  • ref arrives in the props object like any other prop. There's no wrapper and no second argument.
  • useImperativeHandle replaces what the parent sees in ref.current. Instead of the raw <input> (where the parent could change any attribute, style or event), it gets exactly two methods. That keeps the child in control of its own DOM.

The parent

import { useRef } from 'react';
import { SearchBox } from './SearchBox';

export function App() {
  const search = useRef(null);
  return (
    <>
      <SearchBox ref={search} placeholder="Search…" />
      <button onClick={() => { search.current.clear(); search.current.focus(); }}>Clear</button>
    </>
  );
}

Each browser filled the input with "react refs", then clicked Clear:

chromium 153.0.8010.12: after Clear → value "", input focused: true, handle: [focus,clear], console errors: 0
firefox 155.0: after Clear → value "", input focused: true, handle: [focus,clear], console errors: 0
webkit 26.6: after Clear → value "", input focused: true, handle: [focus,clear], console errors: 0

search.current is the object returned from useImperativeHandle, not a DOM element: search.current instanceof HTMLElement was false.

TypeScript

Type the handle once and use it on both sides:

import { useImperativeHandle, useRef, type Ref } from 'react';

export type SearchBoxHandle = {
  focus(): void;
  clear(): void;
};

type SearchBoxProps = {
  ref?: Ref<SearchBoxHandle>;
  placeholder?: string;
};

export function SearchBox({ ref, placeholder }: SearchBoxProps) {
  const input = useRef<HTMLInputElement>(null);

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

  return <input ref={input} placeholder={placeholder} />;
}
import { useRef } from 'react';
import { SearchBox, type SearchBoxHandle } from './SearchBox';

export function App() {
  const search = useRef<SearchBoxHandle>(null);
  return (
    <>
      <SearchBox ref={search} placeholder="Search…" />
      <button onClick={() => search.current?.clear()}>Clear</button>
    </>
  );
}

Calling a method the handle doesn't expose is a compile error, which is one more reason to expose a handle rather than the DOM node:

ts/App.tsx(10,46): error TS2339: Property 'blur' does not exist on type 'SearchBoxHandle'.

In React 19's types, useRef needs an initial value (useRef<SearchBoxHandle>(null)), and ref?: Ref<…> in the props type is all you need. There's no forwardRef<Handle, Props> generic juggling.

What about existing forwardRef code?

It keeps working. A forwardRef component with useImperativeHandle ran fine on React 19.3.0 and printed no warning in development:

forwardRef in React 19.3.0 → still works

Migrating is mechanical: drop the forwardRef(…) wrapper and take ref from the props. The official codemod does it for you: npx codemod react/19/remove-forward-ref --target src/. Libraries that still support React 18 have to keep forwardRef for now.

The StrictMode bug in the classic example

The first version of this post numbered its buttons with a counter outside the component, incremented during render:

let buttonIndex = 0;

function CountedButton() {
  ++buttonIndex;            // side effect during render
  const index = buttonIndex;
  return <button>{`Button ${index}`}</button>;
}

Rendering three of them, with and without <StrictMode>:

no StrictMode: Button 1, Button 2, Button 3
StrictMode: Button 2, Button 4, Button 6
StrictMode, index as prop: Button 1, Button 2, Button 3

In development, StrictMode deliberately renders every component twice to expose exactly this kind of impure render, so the counter skips numbers. React can also re-run renders in production, for example when an update is interrupted. Render must be a pure function of props and state. Pass the number in as a prop (the last line), or use useId() if you only need a unique id.

Ref callbacks can clean up now

Also new in React 19: a ref callback can return a cleanup function, like an effect. It's handy for attaching observers to a DOM node:

<div ref={(node) => {
  const observer = new ResizeObserver(([entry]) => console.log(`resized: ${Math.round(entry.contentRect.width)}px`));
  observer.observe(node);
  return () => {               // runs when the node is removed
    observer.disconnect();
    console.log('disconnected');
  };
}} />

Mounting that 120 px box and then removing it in Chromium logged:

resized: 120px
disconnected

Before React 19, the callback was called again with null on unmount, and you had to keep the observer somewhere yourself.

Often you don't need an imperative call at all

Before reaching for useImperativeHandle, check whether props can do it. The most common case, "reset this form", doesn't need a reset() method. Change the child's key and React mounts a fresh instance with fresh state:

function App() {
  const [version, setVersion] = useState(0);
  return (
    <>
      <Form key={version} />
      <button onClick={() => setVersion((v) => v + 1)}>Reset</button>
    </>
  );
}
typed:       "Ada"
after reset: ""

Keep imperative handles for things that really are imperative in the DOM: focus, selection, scrolling, media playback and animations.

Sources & further reading

About Code with Node.js

This is a personal blog and reference point of a Node.js developer.

I write and explain how different Node and JavaScript aspects work, as well as research popular and cool packages, and of course fail time to time.