Back to Blog
ReactHooksuseImperativeHandleRefs

React useImperativeHandle Hook Guide

Master the React useImperativeHandle hook for customizing ref values exposed to parent components.

B
Bootspring Team
Engineering
October 26, 2019
6 min read

The useImperativeHandle hook customizes the instance value exposed when using ref with forwardRef. Here's how to use it.

Basic Usage#

1import { forwardRef, useImperativeHandle, useRef } from 'react'; 2 3interface InputHandle { 4 focus: () => void; 5 clear: () => void; 6} 7 8const CustomInput = forwardRef<InputHandle, { placeholder?: string }>( 9 (props, ref) => { 10 const inputRef = useRef<HTMLInputElement>(null); 11 12 useImperativeHandle(ref, () => ({ 13 focus: () => { 14 inputRef.current?.focus(); 15 }, 16 clear: () => { 17 if (inputRef.current) { 18 inputRef.current.value = ''; 19 } 20 }, 21 })); 22 23 return <input ref={inputRef} placeholder={props.placeholder} />; 24 } 25); 26 27// Parent component 28function Form() { 29 const inputRef = useRef<InputHandle>(null); 30 31 const handleSubmit = () => { 32 // Use custom methods 33 inputRef.current?.clear(); 34 inputRef.current?.focus(); 35 }; 36 37 return ( 38 <form onSubmit={handleSubmit}> 39 <CustomInput ref={inputRef} placeholder="Enter text" /> 40 <button type="submit">Submit</button> 41 </form> 42 ); 43}

Video Player Control#

1interface VideoHandle { 2 play: () => void; 3 pause: () => void; 4 seek: (time: number) => void; 5 getCurrentTime: () => number; 6} 7 8const VideoPlayer = forwardRef<VideoHandle, { src: string }>((props, ref) => { 9 const videoRef = useRef<HTMLVideoElement>(null); 10 11 useImperativeHandle(ref, () => ({ 12 play: () => { 13 videoRef.current?.play(); 14 }, 15 pause: () => { 16 videoRef.current?.pause(); 17 }, 18 seek: (time: number) => { 19 if (videoRef.current) { 20 videoRef.current.currentTime = time; 21 } 22 }, 23 getCurrentTime: () => { 24 return videoRef.current?.currentTime ?? 0; 25 }, 26 })); 27 28 return <video ref={videoRef} src={props.src} />; 29}); 30 31// Usage 32function VideoPage() { 33 const videoRef = useRef<VideoHandle>(null); 34 35 return ( 36 <div> 37 <VideoPlayer ref={videoRef} src="/video.mp4" /> 38 <button onClick={() => videoRef.current?.play()}>Play</button> 39 <button onClick={() => videoRef.current?.pause()}>Pause</button> 40 <button onClick={() => videoRef.current?.seek(0)}>Restart</button> 41 </div> 42 ); 43}

Form Component#

1interface FormHandle { 2 submit: () => void; 3 reset: () => void; 4 validate: () => boolean; 5 getValues: () => Record<string, string>; 6} 7 8const ManagedForm = forwardRef<FormHandle, { children: React.ReactNode }>( 9 (props, ref) => { 10 const formRef = useRef<HTMLFormElement>(null); 11 const [errors, setErrors] = useState<Record<string, string>>({}); 12 13 useImperativeHandle(ref, () => ({ 14 submit: () => { 15 formRef.current?.requestSubmit(); 16 }, 17 reset: () => { 18 formRef.current?.reset(); 19 setErrors({}); 20 }, 21 validate: () => { 22 const form = formRef.current; 23 if (!form) return false; 24 25 const isValid = form.checkValidity(); 26 if (!isValid) { 27 // Collect errors 28 const newErrors: Record<string, string> = {}; 29 Array.from(form.elements).forEach((el) => { 30 if (el instanceof HTMLInputElement && !el.validity.valid) { 31 newErrors[el.name] = el.validationMessage; 32 } 33 }); 34 setErrors(newErrors); 35 } 36 return isValid; 37 }, 38 getValues: () => { 39 const form = formRef.current; 40 if (!form) return {}; 41 42 const formData = new FormData(form); 43 return Object.fromEntries(formData.entries()) as Record<string, string>; 44 }, 45 })); 46 47 return ( 48 <form ref={formRef} noValidate> 49 {props.children} 50 </form> 51 ); 52 } 53);

Scroll Container#

1interface ScrollHandle { 2 scrollToTop: () => void; 3 scrollToBottom: () => void; 4 scrollTo: (position: number) => void; 5 getScrollPosition: () => number; 6} 7 8const ScrollContainer = forwardRef<ScrollHandle, { children: React.ReactNode }>( 9 (props, ref) => { 10 const containerRef = useRef<HTMLDivElement>(null); 11 12 useImperativeHandle(ref, () => ({ 13 scrollToTop: () => { 14 containerRef.current?.scrollTo({ top: 0, behavior: 'smooth' }); 15 }, 16 scrollToBottom: () => { 17 const container = containerRef.current; 18 if (container) { 19 container.scrollTo({ 20 top: container.scrollHeight, 21 behavior: 'smooth', 22 }); 23 } 24 }, 25 scrollTo: (position: number) => { 26 containerRef.current?.scrollTo({ top: position, behavior: 'smooth' }); 27 }, 28 getScrollPosition: () => { 29 return containerRef.current?.scrollTop ?? 0; 30 }, 31 })); 32 33 return ( 34 <div ref={containerRef} style={{ overflow: 'auto', height: '400px' }}> 35 {props.children} 36 </div> 37 ); 38 } 39);
1interface ModalHandle { 2 open: () => void; 3 close: () => void; 4 toggle: () => void; 5} 6 7const Modal = forwardRef<ModalHandle, { title: string; children: React.ReactNode }>( 8 (props, ref) => { 9 const [isOpen, setIsOpen] = useState(false); 10 const dialogRef = useRef<HTMLDialogElement>(null); 11 12 useImperativeHandle(ref, () => ({ 13 open: () => { 14 dialogRef.current?.showModal(); 15 setIsOpen(true); 16 }, 17 close: () => { 18 dialogRef.current?.close(); 19 setIsOpen(false); 20 }, 21 toggle: () => { 22 if (isOpen) { 23 dialogRef.current?.close(); 24 } else { 25 dialogRef.current?.showModal(); 26 } 27 setIsOpen(!isOpen); 28 }, 29 })); 30 31 return ( 32 <dialog ref={dialogRef}> 33 <h2>{props.title}</h2> 34 {props.children} 35 <button onClick={() => dialogRef.current?.close()}>Close</button> 36 </dialog> 37 ); 38 } 39); 40 41// Usage 42function App() { 43 const modalRef = useRef<ModalHandle>(null); 44 45 return ( 46 <div> 47 <button onClick={() => modalRef.current?.open()}>Open Modal</button> 48 <Modal ref={modalRef} title="My Modal"> 49 <p>Modal content here</p> 50 </Modal> 51 </div> 52 ); 53}

With Dependencies#

1const Counter = forwardRef<{ increment: () => void; getCount: () => number }, {}>( 2 (props, ref) => { 3 const [count, setCount] = useState(0); 4 5 // Include count in dependencies to keep getCount accurate 6 useImperativeHandle( 7 ref, 8 () => ({ 9 increment: () => setCount((c) => c + 1), 10 getCount: () => count, 11 }), 12 [count] // Dependency array 13 ); 14 15 return <div>Count: {count}</div>; 16 } 17);
1interface CarouselHandle { 2 next: () => void; 3 prev: () => void; 4 goTo: (index: number) => void; 5 getCurrentIndex: () => number; 6} 7 8const Carousel = forwardRef<CarouselHandle, { items: React.ReactNode[] }>( 9 (props, ref) => { 10 const [currentIndex, setCurrentIndex] = useState(0); 11 const { items } = props; 12 13 useImperativeHandle( 14 ref, 15 () => ({ 16 next: () => { 17 setCurrentIndex((i) => (i + 1) % items.length); 18 }, 19 prev: () => { 20 setCurrentIndex((i) => (i - 1 + items.length) % items.length); 21 }, 22 goTo: (index: number) => { 23 if (index >= 0 && index < items.length) { 24 setCurrentIndex(index); 25 } 26 }, 27 getCurrentIndex: () => currentIndex, 28 }), 29 [currentIndex, items.length] 30 ); 31 32 return ( 33 <div className="carousel"> 34 <div className="carousel-item">{items[currentIndex]}</div> 35 <div className="carousel-dots"> 36 {items.map((_, i) => ( 37 <span 38 key={i} 39 className={i === currentIndex ? 'active' : ''} 40 onClick={() => setCurrentIndex(i)} 41 /> 42 ))} 43 </div> 44 </div> 45 ); 46 } 47);

Accordion Component#

1interface AccordionHandle { 2 expandAll: () => void; 3 collapseAll: () => void; 4 toggle: (index: number) => void; 5} 6 7const Accordion = forwardRef<AccordionHandle, { items: { title: string; content: React.ReactNode }[] }>( 8 (props, ref) => { 9 const [expanded, setExpanded] = useState<Set<number>>(new Set()); 10 11 useImperativeHandle(ref, () => ({ 12 expandAll: () => { 13 setExpanded(new Set(props.items.map((_, i) => i))); 14 }, 15 collapseAll: () => { 16 setExpanded(new Set()); 17 }, 18 toggle: (index: number) => { 19 setExpanded((prev) => { 20 const next = new Set(prev); 21 if (next.has(index)) { 22 next.delete(index); 23 } else { 24 next.add(index); 25 } 26 return next; 27 }); 28 }, 29 })); 30 31 return ( 32 <div className="accordion"> 33 {props.items.map((item, index) => ( 34 <div key={index} className="accordion-item"> 35 <button 36 onClick={() => { 37 setExpanded((prev) => { 38 const next = new Set(prev); 39 next.has(index) ? next.delete(index) : next.add(index); 40 return next; 41 }); 42 }} 43 > 44 {item.title} 45 </button> 46 {expanded.has(index) && ( 47 <div className="accordion-content">{item.content}</div> 48 )} 49 </div> 50 ))} 51 </div> 52 ); 53 } 54);

Best Practices#

Usage: ✓ Expose minimal API ✓ Use with forwardRef ✓ Include dependencies ✓ Type the handle interface Patterns: ✓ Media controls (play/pause) ✓ Form methods (submit/reset) ✓ Scroll management ✓ Modal open/close When to Use: ✓ Imperative actions needed ✓ Hiding internal implementation ✓ Library component APIs ✓ Animation triggers Avoid: ✗ Exposing entire DOM element ✗ Using for data flow ✗ Complex state management ✗ Replacing props/callbacks

Conclusion#

The useImperativeHandle hook creates a clean imperative API for components. Use it with forwardRef to expose specific methods like focus, scroll, or play/pause controls. Keep the exposed interface minimal and prefer declarative patterns when possible. Always include dependencies in the array when the handle methods reference state or props.

Share this article

Help spread the word about Bootspring