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);Modal Component#
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);Carousel Component#
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.