Mastering React refs: When and How to Use forwardRef, useRef, and useImperativeHandle
This article explains how React refs work with native elements, class components, and function components, demonstrates using useRef, forwardRef, and useImperativeHandle, and discusses the benefits, drawbacks, and alternative patterns for exposing component instances and DOM nodes.
When you pass a ref to a native HTML element like <input> , React attaches the ref to the underlying DOM node, allowing direct DOM API usage.
<code>import React, { useRef, useEffect } from 'react';
const App = () => {
const inputRef = useRef(null);
useEffect(() => {
if (inputRef.current) {
// access DOM node
inputRef.current.focus();
}
}, []);
return <input ref={inputRef} />;
};
</code>In class components, a ref is attached to the component instance, giving access to its properties and methods without extra work.
<code>class ChildComponent extends Component {
doSomething = () => {
console.log('Child method called');
};
}
class ParentComponent extends Component {
childRef = React.createRef();
render() {
return <ChildComponent ref={this.childRef} />;
}
componentDidMount() {
// call child method
if (this.childRef.current) {
this.childRef.current.doSomething();
}
}
}
</code>Function components cannot receive refs as props; React warns because ref is a special attribute not passed through props.
<code>const Child = (props) => {
console.log(props); // outputs {name: '123'}
return <div ref={props.ref}>1231231231</div>;
};
export default () => {
const childRef = useRef(null);
window.childRef = childRef;
return <Child ref={childRef} name="123" />;
};
</code>To make refs work in function components, wrap the component with forwardRef :
<code>import React, { forwardRef, useRef } from 'react';
const Child = forwardRef((props, ref) => {
return <div ref={ref} />;
});
const App = () => {
const childRef = useRef(null);
return <Child ref={childRef} />;
};
</code>Class components can also be wrapped with forwardRef , which is useful for consistency and future migration.
<code>class ChildComponent extends Component {
doSomething = () => {
console.log('Child method called');
};
render() {
return (
<div tabIndex="0" ref={this.props.ref}>Child Component</div>
);
}
}
const ForwardedChildComponent = forwardRef((props, ref) => {
return <ChildComponent {...props} ref={ref} />;
});
</code>Using forwardRef provides benefits such as declarative ref forwarding, compatibility between function and class components, easy DOM access, ability to call instance methods (especially with useImperativeHandle ), avoiding unnecessary re‑renders, smoother integration with third‑party libraries, and future‑proofing for migration to hooks‑based components.
Exposing imperative handles from function components
Function components can expose methods and properties via useImperativeHandle :
<code>function ParentComponent() {
const ref = useRef(null);
function handleRef(refValue) {
// refValue is the current value of ref
}
return <ChildComponent refCallback={handleRef} />;
}
function ChildComponent({ refCallback }) {
useImperativeHandle(refCallback, () => ({
// expose methods/properties here
}));
return <div>Child</div>;
}
</code>A simpler alternative is to use a callback ref or a custom ref prop, which works for both class and function components.
<code>class ChildComponent extends React.Component {
componentDidMount() {
if (this.props.refCallback222) {
this.props.refCallback222(this);
}
}
render() {
return <div>1231231231</div>;
}
}
export default () => {
const childRef = useRef(null);
return (
<ChildComponent
refCallback222={(vm) => (childRef.current = vm)}
name="123"
/>
);
};
</code>Another approach is to use a custom ref prop, such as firstInputRef , which works automatically with function components:
<code>interface ChildProps {
firstInputRef: Ref<HTMLInputElement>;
}
const Child = ({ firstInputRef }: ChildProps) => {
return (
<div>
<input ref={firstInputRef} />
</div>
);
};
const App = () => {
const inputRef = useRef(null);
useEffect(() => {
if (inputRef.current) {
console.log(inputRef.current);
}
}, []);
return <Child firstInputRef={inputRef} />;
};
</code>Conclusion
In React, forwardRef is a higher‑order component that forwards a ref to a child, useful for accessing child methods or DOM nodes, integrating third‑party components, solving deep nesting issues, and maintaining compatibility between function and class components.
It enables imperative access to child instances.
It simplifies DOM manipulation from parent components.
It eases integration with libraries that require refs.
It helps bypass multiple layers of component nesting.
It works with higher‑order components that need to pass refs.
It bridges the gap between pre‑hook and hook‑based components.
drawbacks of forwardRef
Only a single ref can be forwarded, making multiple refs cumbersome.
Arrow functions used in forwardRef appear anonymous in dev tools unless named.
Additional boilerplate increases complexity and reduces readability.
Nesting components with forwarded refs adds unnecessary complexity.
Generic ref names lack descriptiveness.
TypeScript generic inference can become harder.
Potential performance overhead when wrapping many components.
Even if forwardRef were removed from React, most code would still work; a custom ref prop often provides a simpler, more readable solution, and forwardRef is only essential for specific cases such as single‑element proxy components or emulating instance refs.
Code Mala Tang
Read source code together, write articles together, and enjoy spicy hot pot together.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.