Mastering useImperativeHandle
in React: When and How to Use It
React is famous for its declarative approach—describing what the UI should look like based on state. But sometimes, you need to step outside that model and tell React what to do directly. That’s where imperative handles come in.
In this post, we’ll explore how to expose functions from a child component to its parent using useImperativeHandle
, understand when and why to use it, and walk through a practical example of implementing a Scrollable
component with exposed scrollToTop
and scrollToBottom
methods.
There are situations where a parent component needs to directly control a child component. For example:
React offers a solution via refs. Normally, ref
is used to directly access a DOM node, but you can also use it to expose methods from a component to its parent.
You might be tempted to do something like this:
type InputAPI = { focusInput: () => void }function MyInput({ref,...props}: React.InputHTMLAttributes<HTMLInputElement> & {ref: React.RefObject<InputAPI>}) {const inputRef = useRef<HTMLInputElement>(null)ref.current = {focusInput: () => inputRef.current?.focus(),}return <input ref={inputRef} {...props} />}
This works, but it’s not safe with concurrent features or callback refs. React warns against mutating the .current
property of refs in this way inside child components.
useImperativeHandle
React provides the useImperativeHandle
hook to safely expose methods to a parent through a ref.
type InputAPI = { focusInput: () => void }const MyInput = React.forwardRef<InputAPI, React.InputHTMLAttributes<HTMLInputElement>>((props, ref) => {const inputRef = useRef<HTMLInputElement>(null)useImperativeHandle(ref, () => ({focusInput: () => inputRef.current?.focus(),}), [])return <input ref={inputRef} {...props} />})
🔒
useImperativeHandle
makes your component API safer and better encapsulated.
You might notice that inputRef
is used inside the function passed to useImperativeHandle
, but it’s not listed in the dependency array. That’s because you don’t need to include refs—they are stable and won’t change between renders.
Let’s say we’re building a component that wraps a scrollable container and gives parent components the ability to scroll to the top or bottom.
You’re given a Scrollable
component with two already-implemented methods: scrollToTop()
and scrollToBottom()
. Your job is to expose these methods to the parent using useImperativeHandle
.
type ScrollAPI = {scrollToTop: () => voidscrollToBottom: () => void}const Scrollable = React.forwardRef<ScrollAPI, React.HTMLAttributes<HTMLDivElement>>((props, ref) => {const containerRef = useRef<HTMLDivElement>(null)const scrollToTop = () => {containerRef.current?.scrollTo({ top: 0, behavior: 'smooth' })}const scrollToBottom = () => {containerRef.current?.scrollTo({top: containerRef.current.scrollHeight,behavior: 'smooth',})}useImperativeHandle(ref, () => ({scrollToTop,scrollToBottom,}), [])return (<divref={containerRef}style={{ overflowY: 'auto', maxHeight: '200px', border: '1px solid #ccc' }}{...props}/>)})
Scrollable
Componentfunction App() {const scrollRef = useRef<ScrollAPI>(null)return (<div><button onClick={() => scrollRef.current?.scrollToTop()}>Scroll to Top</button><button onClick={() => scrollRef.current?.scrollToBottom()}>Scroll to Bottom</button><Scrollable ref={scrollRef}>{/* Long content here */}<div style={{ height: '600px' }}><p>Lots of content here...</p></div></Scrollable></div>)}
useImperativeHandle
?🛑 Don’t overuse it! Most of the time, you shouldn’t need it.
Prefer declarative solutions. Only use useImperativeHandle
when:
Read more about this concept here: 👉 Imperative vs Declarative Programming
useRef
stores a reference that persists across renders.useImperativeHandle
lets you expose methods to parent components safely.React.forwardRef
.By understanding and using useImperativeHandle
, you unlock more advanced control patterns in React—just remember to wield it responsibly!
Quick Links
Legal Stuff
Social Media