When Parents Need to Touch Children's DOM: forwardRef & useImperativeHandle
1. "Focus the Input When the Modal Opens"
As a junior developer, my mentor gave me a task that sounded simple.
"When the user clicks the login button and the modal opens, make the cursor blink in the ID input field immediately. Don't make them click twice."
"Easy!" I replied.
I knew about React's useRef. It's the standard Hook used when you need to touch the real DOM directly (focusing, scrolling, playing media, etc.).
So I created a ref in the parent LoginModal component and tried to pass it to the child CustomInput component.
/* ❌ Failed Attempt: Coding by intuition */
function LoginModal() {
const inputRef = useRef(null);
const openModal = () => {
setShowModal(true);
// Focus when modal opens! (Or so I thought)
inputRef.current.focus();
};
// Passing it as 'ref' (Thought it would work like className)
return (
<div className="modal">
<CustomInput ref={inputRef} placeholder="User ID" />
</div>
);
}
// Child Component (Our pretty input wrapper)
function CustomInput(props) {
// ❌ props.ref? It doesn't exist. undefined.
return <input className="fancy-input" ref={props.ref} {...props} />;
}
The result? An Error Party.
The console shouted a red warning, and inputRef.current remained undefined forever. No matter how many times I clicked the button, focus just wouldn't happen.
Warning: Function components cannot be given refs. Attempts to access this ref will fail. Did you mean to use React.forwardRef()?
2. Refs Need Special Treatment
At first, I thought React was broken.
className, style, onClick, value all pass through props just fine, so why is ref being discriminated against?
It turned out that ref is a reserved word internally handled by React, just like key.
When React renders a component and sees a ref attribute, it strips it out before creating the props object. So from the child component's perspective, checking props.ref returns nothing.
I used a cheat. I renamed it to customRef or innerRef.
/* 😅 Using a cheat */
<CustomInput innerRef={inputRef} />
function CustomInput({ innerRef, ...props }) {
return <input ref={innerRef} {...props} />;
}
It worked fine. But it felt likely dirty code.
"Is this a standard pattern? Does everyone just name it customRef, inputRef, innerRef however they like? What should I name it if I'm building a library?"
It wasn't standard. React provided an official solution called forwardRef.
3. Digging the Tunnel: forwardRef
The concept of forwardRef is very intuitive.
"It digs a tunnel to Pass (Toss) the ref received from the parent down to the child."
When you wrap your component with this Higher-Order Component (HOC), React recognizes, "Ah, this one is ready to accept a ref," and passes the ref as a second argument separate from props.
import { forwardRef } from 'react';
/* ✅ Successful Tunnel */
// Receives two arguments: (props, ref)
const CustomInput = forwardRef((props, ref) => {
// 'ref' here is the exact inputRef sent by the parent.
// We plug this into the real <input> tag.
return <input ref={ref} className="fancy-input" {...props} />;
});
// App.js
function LoginModal() {
const inputRef = useRef(null);
// No error now! CustomInput can accept ref naturally.
return <CustomInput ref={inputRef} />;
}
Now the parent component can plug a ref into CustomInput as naturally as if it were a standard <input> tag.
The path to direct DOM access has been opened.
What about TypeScript?
If you are a TypeScript user, defining generic types for forwardRef can be confusing.
Remember the order: forwardRef<RefType, PropsType>. (Yes, it's confusing. Ref comes first!)
// HTMLInputElement is the Ref type, Props is second
const CustomInput = forwardRef<HTMLInputElement, InputProps>((props, ref) => {
return <input ref={ref} {...props} />;
});
React 19 Update: Goodbye forwardRef?
Starting with React 19, forwardRef will become obsolete. You will finally be able to pass ref just like any other prop.
However, since many projects still run on React 18 or older, and library ecosystems take time to migrate, you will continue seeing the forwardRef pattern for years to come.
/* React 19 Style (So much cleaner!) */
function CustomInput({ ref, ...props }) {
return <input ref={ref} {...props} />;
}
4. Common Mistake: "Why is ref.current null?"
Even with forwardRef, you might encounter Cannot read properties of null.
The culprit is usually Timing.
// ❌ Don't access ref during render!
function App() {
const ref = useRef(null);
ref.current.focus(); // 💥 Error! DOM is not painted yet.
return <CustomInput ref={ref} />;
}
Refs are populated immediately after the DOM is painted (Commit phase).
So, strictly access them inside useEffect or Event Handlers (onClick).
// ✅ Safe Access
useEffect(() => {
if (ref.current) {
ref.current.focus();
}
}, []);
5. But... Should I Give Away My Raw DOM?
After solving the problem with forwardRef, my Senior Developer looked at the code and said:
"If you expose the child's internal implementation (DOM) to the parent, isn't that breaking encapsulation?"
Thinking about it, he was right.
Giving the parent inputRef.current means giving them full authority over the <input> DOM element.
The parent component could suddenly do something crazy:
inputRef.current.style.display = 'none'; // Hides it arbitrarily
inputRef.current.value = 'Hacked'; // Changes value arbitrarily
inputRef.current.className = ''; // Wipes out styling
inputRef.current.remove(); // Can even delete it?!
This violates Encapsulation.
The child component might complain, "I only wanted to allow you to 'Focus', why do I have to show you my naked self (DOM)?"
Especially if the child component changes its structure later (e.g., using a textarea instead of input, or adding complex wrappers), the parent code relying on raw DOM access will break. The Coupling became too high.
5. Hiring a Bouncer: useImperativeHandle
This is where useImperativeHandle comes in.
The name is scary and long, but its role is simple.
It declares: "I (the child) will decide which methods the parent can use via the ref."
useImperativeHandle = "Customizing the Handle to be used Imperatively".
Instead of giving the parent the real DOM (HTMLInputElement), I give them a Fake Object (Proxy/Interface) I created.
import { useImperativeHandle, useRef } from 'react';
const CustomInput = forwardRef((props, ref) => {
const realInputRef = useRef(null); // I (Child) hold the real DOM
// Only this object is exposed to the parent (connected to ref)
useImperativeHandle(ref, () => ({
// 1. Only allow 'focus'
focus: () => {
realInputRef.current.focus();
},
// 2. Provide custom methods like animation
shake: () => {
realInputRef.current.classList.add('shake');
setTimeout(() => realInputRef.current.classList.remove('shake'), 500);
},
// * value change or style access is NOT provided!
}));
return <input ref={realInputRef} {...props} />;
});
Now if the parent logs inputRef.current, there is no style, value, or nextSibling. There are only focus and shake functions.
// Parent Component
const onClick = () => {
inputRef.current.focus(); // OK
inputRef.current.shake(); // OK
inputRef.current.style.color = 'red'; // ❌ Error! style is undefined
};
The child component hides its internal implementation and exposes only a safe API (Public Interface). This is true Object-Oriented Design.
Efficiency Note
Does forwardRef make your app slower?
Not at all. It's just a function wrapper.
However, if you reconstruct the object inside useImperativeHandle on every render without dependency array, it might trigger unnecessary effect re-runs in the parent. Always double-check your dependencies (usually [] or [props.someValue]).
6. Real-world Use Case: Video Player
I used this pattern most effectively when building a Custom Video Player.
The HTML <video> tag has a ton of controls: play(), pause(), currentTime, volume, playbackRate, etc.
But letting the parent component access the <video> tag directly risks unintended changes to src or other critical attributes that could break the player. Or maybe you want to normalize behavior across different browsers.
const VideoPlayer = forwardRef((props, ref) => {
const videoRef = useRef(null);
useImperativeHandle(ref, () => ({
play: () => {
console.log("Analytics: Play clicked"); // Can add logging
videoRef.current.play();
},
pause: () => videoRef.current.pause(),
seekTo: (time) => {
videoRef.current.currentTime = time;
},
restart: () => { // Custom convenience method
videoRef.current.currentTime = 0;
videoRef.current.play();
}
}));
return (
<div className="player-wrapper">
<video ref={videoRef} src={props.src} />
{/* Custom Controller UIs */}
</div>
);
});
This way, the parent component can only issue High-level Commands (Imperative) like "Play", "Stop", "Seek", without needing to know how the video actually works underneath. Whether the internal implementation switches to a YouTube player or Vimeo, the parent code remains safe.
A Quick Note on Ref Callbacks
Sometimes useRef isn't enough. If you need to know exactly when a DOM node is mounted or unmounted (e.g., to measure its size), use a Callback Ref instead of a Ref Object.
<div ref={(node) => {
if (node) {
console.log("Mounted!", node.getBoundingClientRect());
} else {
console.log("Unmounted!");
}
}} />
This pattern works perfectly fine with forwardRef too!
Troubleshooting with React DevTools
If you are unsure whether your forwardRef is working correctly, open React DevTools.
Components wrapped in forwardRef might appear as ForwardRef in the component tree.
To make debugging easier, you can assign a displayName:
const MyComponent = forwardRef((props, ref) => { ... });
MyComponent.displayName = 'MyComponent';
This simple trick saves you from hunting down "Anonymous" components in the DevTools tree.
Always check your console for warnings too.
7. Testing & Accessibility (Hidden Gems)
Testing with React Testing Library
Testing components with forwardRef is straightforward but requires knowing what to look for.
Scenario: You want to verify that focus() works.
test('should focus input when method is called', () => {
const ref = React.createRef();
render(<CustomInput ref={ref} />);
// Act
act(() => {
ref.current.focus();
});
// Assert
expect(screen.getByRole('textbox')).toHaveFocus();
});
If you use useImperativeHandle, you might want to mock the ref to check if your custom methods exist.
Accessibility (A11y) and Refs
Refs are the backbone of accessible web apps.
- Focus Management: When a modal opens, you MUST move focus to it. When it closes, you MUST return focus to the button that opened it. Refs make this possible.
- aria-activedescendant: For complex widgets like Comboboxes, you need to manage active items programmatically, often using refs to scroll them into view.
Pro Tip: If you are building a UI library, forwardRef isn't optional—it's mandatory. Consumers of your library will need access to the DOM for accessibility tools and animations.
7. One-Line Summary
If you need to access a child component's DOM or methods, dig a tunnel with forwardRef, and hire a bouncer with useImperativeHandle to expose only safe and necessary features.
Mastering this pattern allows you to write elegant Imperative code when needed, without breaking React's preferred Unidirectional Data Flow (Props Down).