Refs provide direct access to DOM elements and component instances. Here's how to use them effectively.
Basic useRef#
1import { useRef, useEffect } from 'react';
2
3function TextInput() {
4 const inputRef = useRef<HTMLInputElement>(null);
5
6 useEffect(() => {
7 // Focus on mount
8 inputRef.current?.focus();
9 }, []);
10
11 const handleClick = () => {
12 // Access DOM directly
13 inputRef.current?.select();
14 };
15
16 return (
17 <div>
18 <input ref={inputRef} type="text" />
19 <button onClick={handleClick}>Select All</button>
20 </div>
21 );
22}
23
24// Store mutable values without re-renders
25function Timer() {
26 const intervalRef = useRef<NodeJS.Timeout | null>(null);
27 const countRef = useRef(0);
28
29 const startTimer = () => {
30 intervalRef.current = setInterval(() => {
31 countRef.current += 1;
32 console.log(countRef.current);
33 }, 1000);
34 };
35
36 const stopTimer = () => {
37 if (intervalRef.current) {
38 clearInterval(intervalRef.current);
39 }
40 };
41
42 return (
43 <div>
44 <button onClick={startTimer}>Start</button>
45 <button onClick={stopTimer}>Stop</button>
46 </div>
47 );
48}Callback Refs#
1import { useCallback, useState } from 'react';
2
3function MeasuredComponent() {
4 const [height, setHeight] = useState(0);
5
6 // Callback ref - called when element mounts/unmounts
7 const measuredRef = useCallback((node: HTMLDivElement | null) => {
8 if (node !== null) {
9 setHeight(node.getBoundingClientRect().height);
10 }
11 }, []);
12
13 return (
14 <div>
15 <div ref={measuredRef}>
16 Content that we want to measure
17 </div>
18 <p>Height: {height}px</p>
19 </div>
20 );
21}
22
23// Handling multiple refs
24function MultipleRefs() {
25 const refsMap = useRef(new Map<string, HTMLDivElement>());
26
27 const setRef = (id: string) => (node: HTMLDivElement | null) => {
28 if (node) {
29 refsMap.current.set(id, node);
30 } else {
31 refsMap.current.delete(id);
32 }
33 };
34
35 const scrollToItem = (id: string) => {
36 const node = refsMap.current.get(id);
37 node?.scrollIntoView({ behavior: 'smooth' });
38 };
39
40 return (
41 <div>
42 {['a', 'b', 'c'].map((id) => (
43 <div key={id} ref={setRef(id)}>
44 Item {id}
45 </div>
46 ))}
47 <button onClick={() => scrollToItem('c')}>Go to C</button>
48 </div>
49 );
50}forwardRef#
1import { forwardRef, useRef } from 'react';
2
3// Forward ref to child component
4interface InputProps {
5 label: string;
6 placeholder?: string;
7}
8
9const FancyInput = forwardRef<HTMLInputElement, InputProps>(
10 ({ label, placeholder }, ref) => {
11 return (
12 <div className="fancy-input">
13 <label>{label}</label>
14 <input ref={ref} placeholder={placeholder} />
15 </div>
16 );
17 }
18);
19
20FancyInput.displayName = 'FancyInput';
21
22// Usage
23function Form() {
24 const inputRef = useRef<HTMLInputElement>(null);
25
26 const focusInput = () => {
27 inputRef.current?.focus();
28 };
29
30 return (
31 <div>
32 <FancyInput
33 ref={inputRef}
34 label="Username"
35 placeholder="Enter username"
36 />
37 <button onClick={focusInput}>Focus Input</button>
38 </div>
39 );
40}useImperativeHandle#
1import { forwardRef, useImperativeHandle, useRef } from 'react';
2
3// Define handle type
4interface ModalHandle {
5 open: () => void;
6 close: () => void;
7 toggle: () => void;
8}
9
10interface ModalProps {
11 title: string;
12 children: React.ReactNode;
13}
14
15const Modal = forwardRef<ModalHandle, ModalProps>(
16 ({ title, children }, ref) => {
17 const [isOpen, setIsOpen] = useState(false);
18 const dialogRef = useRef<HTMLDialogElement>(null);
19
20 // Expose custom methods via ref
21 useImperativeHandle(ref, () => ({
22 open: () => {
23 setIsOpen(true);
24 dialogRef.current?.showModal();
25 },
26 close: () => {
27 setIsOpen(false);
28 dialogRef.current?.close();
29 },
30 toggle: () => {
31 if (isOpen) {
32 dialogRef.current?.close();
33 } else {
34 dialogRef.current?.showModal();
35 }
36 setIsOpen(!isOpen);
37 },
38 }), [isOpen]);
39
40 return (
41 <dialog ref={dialogRef}>
42 <h2>{title}</h2>
43 {children}
44 <button onClick={() => dialogRef.current?.close()}>
45 Close
46 </button>
47 </dialog>
48 );
49 }
50);
51
52// Usage
53function App() {
54 const modalRef = useRef<ModalHandle>(null);
55
56 return (
57 <div>
58 <button onClick={() => modalRef.current?.open()}>
59 Open Modal
60 </button>
61 <Modal ref={modalRef} title="My Modal">
62 <p>Modal content here</p>
63 </Modal>
64 </div>
65 );
66}Video Player Example#
1import { forwardRef, useImperativeHandle, useRef } from 'react';
2
3interface VideoPlayerHandle {
4 play: () => void;
5 pause: () => void;
6 seek: (time: number) => void;
7 getCurrentTime: () => number;
8 getDuration: () => number;
9}
10
11interface VideoPlayerProps {
12 src: string;
13 poster?: string;
14}
15
16const VideoPlayer = forwardRef<VideoPlayerHandle, VideoPlayerProps>(
17 ({ src, poster }, ref) => {
18 const videoRef = useRef<HTMLVideoElement>(null);
19
20 useImperativeHandle(ref, () => ({
21 play: () => {
22 videoRef.current?.play();
23 },
24 pause: () => {
25 videoRef.current?.pause();
26 },
27 seek: (time: number) => {
28 if (videoRef.current) {
29 videoRef.current.currentTime = time;
30 }
31 },
32 getCurrentTime: () => {
33 return videoRef.current?.currentTime ?? 0;
34 },
35 getDuration: () => {
36 return videoRef.current?.duration ?? 0;
37 },
38 }), []);
39
40 return (
41 <video
42 ref={videoRef}
43 src={src}
44 poster={poster}
45 controls
46 />
47 );
48 }
49);
50
51// Usage
52function VideoPage() {
53 const playerRef = useRef<VideoPlayerHandle>(null);
54
55 const skipForward = () => {
56 const current = playerRef.current?.getCurrentTime() ?? 0;
57 playerRef.current?.seek(current + 10);
58 };
59
60 return (
61 <div>
62 <VideoPlayer
63 ref={playerRef}
64 src="/video.mp4"
65 />
66 <button onClick={() => playerRef.current?.play()}>Play</button>
67 <button onClick={() => playerRef.current?.pause()}>Pause</button>
68 <button onClick={skipForward}>+10s</button>
69 </div>
70 );
71}Form Control Example#
1import { forwardRef, useImperativeHandle, useRef, useState } from 'react';
2
3interface FormHandle {
4 submit: () => void;
5 reset: () => void;
6 validate: () => boolean;
7 getValues: () => Record<string, string>;
8}
9
10interface FormProps {
11 onSubmit: (data: Record<string, string>) => void;
12 children: React.ReactNode;
13}
14
15const Form = forwardRef<FormHandle, FormProps>(
16 ({ onSubmit, children }, ref) => {
17 const formRef = useRef<HTMLFormElement>(null);
18 const [errors, setErrors] = useState<Record<string, string>>({});
19
20 useImperativeHandle(ref, () => ({
21 submit: () => {
22 formRef.current?.requestSubmit();
23 },
24 reset: () => {
25 formRef.current?.reset();
26 setErrors({});
27 },
28 validate: () => {
29 if (!formRef.current) return false;
30 return formRef.current.checkValidity();
31 },
32 getValues: () => {
33 if (!formRef.current) return {};
34 const formData = new FormData(formRef.current);
35 return Object.fromEntries(formData.entries()) as Record<string, string>;
36 },
37 }), []);
38
39 const handleSubmit = (e: React.FormEvent) => {
40 e.preventDefault();
41 if (!formRef.current) return;
42
43 const formData = new FormData(formRef.current);
44 const data = Object.fromEntries(formData.entries()) as Record<string, string>;
45 onSubmit(data);
46 };
47
48 return (
49 <form ref={formRef} onSubmit={handleSubmit}>
50 {children}
51 </form>
52 );
53 }
54);
55
56// Usage
57function App() {
58 const formRef = useRef<FormHandle>(null);
59
60 const handleSave = () => {
61 if (formRef.current?.validate()) {
62 formRef.current.submit();
63 } else {
64 alert('Please fix validation errors');
65 }
66 };
67
68 return (
69 <div>
70 <Form ref={formRef} onSubmit={(data) => console.log(data)}>
71 <input name="email" type="email" required />
72 <input name="password" type="password" required />
73 </Form>
74 <button onClick={handleSave}>Save</button>
75 <button onClick={() => formRef.current?.reset()}>Reset</button>
76 </div>
77 );
78}Merging Refs#
1import { useRef, useCallback, RefCallback, MutableRefObject } from 'react';
2
3// Utility to merge multiple refs
4function mergeRefs<T>(
5 ...refs: (RefCallback<T> | MutableRefObject<T> | null)[]
6): RefCallback<T> {
7 return (instance: T) => {
8 refs.forEach((ref) => {
9 if (typeof ref === 'function') {
10 ref(instance);
11 } else if (ref != null) {
12 ref.current = instance;
13 }
14 });
15 };
16}
17
18// Usage with forwardRef
19interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {}
20
21const Button = forwardRef<HTMLButtonElement, ButtonProps>((props, ref) => {
22 const internalRef = useRef<HTMLButtonElement>(null);
23
24 // Use both refs
25 const mergedRef = mergeRefs(ref, internalRef);
26
27 return <button ref={mergedRef} {...props} />;
28});
29
30// Hook version
31function useMergedRef<T>(...refs: (RefCallback<T> | MutableRefObject<T> | null)[]) {
32 return useCallback(
33 (instance: T) => {
34 refs.forEach((ref) => {
35 if (typeof ref === 'function') {
36 ref(instance);
37 } else if (ref != null) {
38 ref.current = instance;
39 }
40 });
41 },
42 // eslint-disable-next-line react-hooks/exhaustive-deps
43 refs
44 );
45}Avoiding Common Pitfalls#
1// DON'T: Access ref in render
2function Bad() {
3 const ref = useRef<HTMLInputElement>(null);
4
5 // ref.current is null during render!
6 console.log(ref.current?.value); // undefined
7
8 return <input ref={ref} />;
9}
10
11// DO: Access ref in effects or handlers
12function Good() {
13 const ref = useRef<HTMLInputElement>(null);
14
15 useEffect(() => {
16 console.log(ref.current?.value); // Works
17 }, []);
18
19 const handleClick = () => {
20 console.log(ref.current?.value); // Works
21 };
22
23 return <input ref={ref} />;
24}
25
26// DON'T: Use refs for reactive data
27function BadReactive() {
28 const countRef = useRef(0);
29
30 // This won't update the UI!
31 const increment = () => {
32 countRef.current += 1;
33 };
34
35 return <p>{countRef.current}</p>;
36}
37
38// DO: Use state for reactive data
39function GoodReactive() {
40 const [count, setCount] = useState(0);
41
42 const increment = () => {
43 setCount(c => c + 1);
44 };
45
46 return <p>{count}</p>;
47}Best Practices#
When to Use Refs:
✓ DOM element access (focus, scroll, measure)
✓ Storing timeout/interval IDs
✓ Storing previous values
✓ Imperative animations
✓ Third-party library integration
When NOT to Use Refs:
✗ Data that affects rendering (use state)
✗ Avoiding re-renders (consider memo)
✗ Storing component state
✗ Communication between components
forwardRef:
✓ Use for reusable components
✓ Add displayName for debugging
✓ Document exposed ref API
✓ Consider useImperativeHandle
useImperativeHandle:
✓ Expose minimal API
✓ Hide implementation details
✓ Type the handle interface
✓ Include cleanup in return
Conclusion#
Refs provide escape hatches for DOM access and imperative operations. Use useRef for direct element access, forwardRef to pass refs through components, and useImperativeHandle to expose custom APIs. Remember that refs don't trigger re-renders and should be accessed in effects or event handlers.