React - useRef and forwardRef
Overview
Estimated time: 20–30 minutes
Refs let you hold mutable values across renders and access DOM nodes. Forwarding refs makes library components ref-friendly. Use useImperativeHandle
to expose safe imperative APIs.
Try it: Focus input with useRef
View source
function FocusDemo(){
const inputRef = React.useRef(null);
return (
<div>
<input ref={inputRef} placeholder="Click the button to focus me" />
<button onClick={() => inputRef.current && inputRef.current.focus()} style={{marginLeft:8}}>Focus</button>
</div>
);
}
ReactDOM.createRoot(document.getElementById('try-ref')).render(<FocusDemo />);
Try it: forwardRef + useImperativeHandle
View source
const FancyInput = React.forwardRef(function FancyInput(props, ref){
const innerRef = React.useRef(null);
React.useImperativeHandle(ref, () => ({
focus: () => innerRef.current && innerRef.current.focus(),
value: () => innerRef.current ? innerRef.current.value : ''
}));
return <input ref={innerRef} {...props} />;
});
function Parent(){
const api = React.useRef(null);
return (
<div>
<FancyInput ref={api} placeholder="Forwarded ref input" />
<button onClick={() => api.current && api.current.focus()} style={{marginLeft:8}}>Focus</button>
<button onClick={() => alert(api.current ? api.current.value() : '')} style={{marginLeft:8}}>Get Value</button>
</div>
);
}
ReactDOM.createRoot(document.getElementById('try-forwardref')).render(<Parent />);
Syntax primer
useRef(initial)
stores a mutable.current
that persists across renders.forwardRef
passes a parent ref down to a child component.useImperativeHandle
limits the imperative API exposed by a ref for safety.
Common pitfalls
- Overusing refs for state—prefer state unless you must escape React’s render cycle.
- Exposing full DOM nodes unnecessarily—prefer a narrow imperative handle.
Checks for Understanding
- When should you prefer state over refs?
- Why use
useImperativeHandle
instead of exposing a full DOM node?
Show answers
- When UI should update based on changes—state triggers re-renders; refs do not.
- To provide a minimal, safe imperative API and hide internal implementation details.
Exercises
- Create a
Timer
with a ref exposingstart()
,stop()
, andreset()
. - Expose a limited API from a complex input (e.g.,
clear()
,focus()
) usinguseImperativeHandle
. - Build a form that focuses the first invalid field using refs.