Call a child component's method from the parent in React 19: ref as a prop + useImperativeHandle
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:
refarrives in the props object like any other prop. There's no wrapper and no second argument.useImperativeHandlereplaces what the parent sees inref.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: 0search.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 worksMigrating 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 3In 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
disconnectedBefore 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
- React docs: useImperativeHandle
- React docs: forwardRef (no longer necessary in React 19)
- React 19 release: ref as a prop and cleanup functions for refs
- React docs: StrictMode (why components render twice in development)
- React docs: resetting state with a key
- React 19 upgrade guide and the remove-forward-ref codemod