Refs provide direct access to DOM elements and persist values across renders. Here's how to use them.
Basic useRef#
1import { useRef, useEffect } from 'react';
2
3function TextInput() {
4 const inputRef = useRef<HTMLInputElement>(null);
5
6 useEffect(() => {
7 // Focus input on mount
8 inputRef.current?.focus();
9 }, []);
10
11 return <input ref={inputRef} type="text" />;
12}
13
14// Storing mutable values
15function Timer() {
16 const intervalRef = useRef<number | null>(null);
17 const [count, setCount] = useState(0);
18
19 useEffect(() => {
20 intervalRef.current = window.setInterval(() => {
21 setCount(c => c + 1);
22 }, 1000);
23
24 return () => {
25 if (intervalRef.current) {
26 clearInterval(intervalRef.current);
27 }
28 };
29 }, []);
30
31 const stop = () => {
32 if (intervalRef.current) {
33 clearInterval(intervalRef.current);
34 }
35 };
36
37 return (
38 <div>
39 <p>Count: {count}</p>
40 <button onClick={stop}>Stop</button>
41 </div>
42 );
43}forwardRef#
1import { forwardRef, useRef } from 'react';
2
3// Forward ref to inner element
4const FancyInput = forwardRef<HTMLInputElement, { label: string }>(
5 function FancyInput({ label }, ref) {
6 return (
7 <div className="fancy-input">
8 <label>{label}</label>
9 <input ref={ref} type="text" />
10 </div>
11 );
12 }
13);
14
15// Usage
16function Form() {
17 const inputRef = useRef<HTMLInputElement>(null);
18
19 const focus = () => {
20 inputRef.current?.focus();
21 };
22
23 return (
24 <div>
25 <FancyInput ref={inputRef} label="Name" />
26 <button onClick={focus}>Focus</button>
27 </div>
28 );
29}
30
31// Forward ref with generic types
32interface InputProps<T> {
33 value: T;
34 onChange: (value: T) => void;
35}
36
37const GenericInput = forwardRef(function GenericInput<T>(
38 props: InputProps<T>,
39 ref: React.ForwardedRef<HTMLInputElement>
40) {
41 return <input ref={ref} />;
42});useImperativeHandle#
1import { forwardRef, useImperativeHandle, useRef } from 'react';
2
3interface ModalHandle {
4 open: () => void;
5 close: () => void;
6}
7
8const Modal = forwardRef<ModalHandle, { children: React.ReactNode }>(
9 function Modal({ children }, ref) {
10 const [isOpen, setIsOpen] = useState(false);
11
12 useImperativeHandle(ref, () => ({
13 open: () => setIsOpen(true),
14 close: () => setIsOpen(false),
15 }), []);
16
17 if (!isOpen) return null;
18
19 return (
20 <div className="modal">
21 {children}
22 <button onClick={() => setIsOpen(false)}>Close</button>
23 </div>
24 );
25 }
26);
27
28// Usage
29function App() {
30 const modalRef = useRef<ModalHandle>(null);
31
32 return (
33 <div>
34 <button onClick={() => modalRef.current?.open()}>
35 Open Modal
36 </button>
37 <Modal ref={modalRef}>
38 <h2>Modal Content</h2>
39 </Modal>
40 </div>
41 );
42}
43
44// Video player example
45interface VideoPlayerHandle {
46 play: () => void;
47 pause: () => void;
48 seek: (time: number) => void;
49 getCurrentTime: () => number;
50}
51
52const VideoPlayer = forwardRef<VideoPlayerHandle, { src: string }>(
53 function VideoPlayer({ src }, ref) {
54 const videoRef = useRef<HTMLVideoElement>(null);
55
56 useImperativeHandle(ref, () => ({
57 play: () => videoRef.current?.play(),
58 pause: () => videoRef.current?.pause(),
59 seek: (time) => {
60 if (videoRef.current) {
61 videoRef.current.currentTime = time;
62 }
63 },
64 getCurrentTime: () => videoRef.current?.currentTime ?? 0,
65 }), []);
66
67 return <video ref={videoRef} src={src} />;
68 }
69);Ref Callbacks#
1// Callback ref for dynamic refs
2function MeasureExample() {
3 const [height, setHeight] = useState(0);
4
5 const measuredRef = useCallback((node: HTMLDivElement | null) => {
6 if (node !== null) {
7 setHeight(node.getBoundingClientRect().height);
8 }
9 }, []);
10
11 return (
12 <div>
13 <div ref={measuredRef}>Content to measure</div>
14 <p>Height: {height}px</p>
15 </div>
16 );
17}
18
19// Multiple refs for list items
20function ListWithRefs() {
21 const itemRefs = useRef<Map<string, HTMLLIElement>>(new Map());
22
23 const setItemRef = useCallback((id: string, node: HTMLLIElement | null) => {
24 if (node) {
25 itemRefs.current.set(id, node);
26 } else {
27 itemRefs.current.delete(id);
28 }
29 }, []);
30
31 const scrollToItem = (id: string) => {
32 const node = itemRefs.current.get(id);
33 node?.scrollIntoView({ behavior: 'smooth' });
34 };
35
36 return (
37 <ul>
38 {items.map(item => (
39 <li
40 key={item.id}
41 ref={(node) => setItemRef(item.id, node)}
42 >
43 {item.name}
44 </li>
45 ))}
46 </ul>
47 );
48}Combining Multiple Refs#
1// Merge multiple refs
2function useMergeRefs<T>(...refs: React.Ref<T>[]) {
3 return useCallback((node: T | null) => {
4 refs.forEach(ref => {
5 if (typeof ref === 'function') {
6 ref(node);
7 } else if (ref) {
8 (ref as React.MutableRefObject<T | null>).current = node;
9 }
10 });
11 }, refs);
12}
13
14// Usage
15const Input = forwardRef<HTMLInputElement, InputProps>(
16 function Input(props, forwardedRef) {
17 const localRef = useRef<HTMLInputElement>(null);
18 const mergedRef = useMergeRefs(localRef, forwardedRef);
19
20 useEffect(() => {
21 // Can use localRef internally
22 localRef.current?.focus();
23 }, []);
24
25 return <input ref={mergedRef} {...props} />;
26 }
27);Ref with Intersection Observer#
1function useIntersectionObserver<T extends Element>(
2 options?: IntersectionObserverInit
3) {
4 const [entry, setEntry] = useState<IntersectionObserverEntry | null>(null);
5 const elementRef = useRef<T | null>(null);
6
7 useEffect(() => {
8 const element = elementRef.current;
9 if (!element) return;
10
11 const observer = new IntersectionObserver(
12 ([entry]) => setEntry(entry),
13 options
14 );
15
16 observer.observe(element);
17 return () => observer.disconnect();
18 }, [options?.root, options?.rootMargin, options?.threshold]);
19
20 return { ref: elementRef, entry };
21}
22
23// Usage
24function LazyImage({ src, alt }: { src: string; alt: string }) {
25 const { ref, entry } = useIntersectionObserver<HTMLImageElement>({
26 rootMargin: '100px',
27 });
28
29 const isVisible = entry?.isIntersecting ?? false;
30
31 return (
32 <img
33 ref={ref}
34 src={isVisible ? src : undefined}
35 alt={alt}
36 loading="lazy"
37 />
38 );
39}Previous Value Ref#
1function usePrevious<T>(value: T): T | undefined {
2 const ref = useRef<T>();
3
4 useEffect(() => {
5 ref.current = value;
6 }, [value]);
7
8 return ref.current;
9}
10
11// Usage
12function Counter() {
13 const [count, setCount] = useState(0);
14 const prevCount = usePrevious(count);
15
16 return (
17 <div>
18 <p>Current: {count}, Previous: {prevCount ?? 'N/A'}</p>
19 <button onClick={() => setCount(c => c + 1)}>Increment</button>
20 </div>
21 );
22}Latest Value Ref#
1// Keep ref in sync with latest value
2function useLatest<T>(value: T) {
3 const ref = useRef(value);
4 ref.current = value;
5 return ref;
6}
7
8// Usage in callbacks
9function Search({ onSearch }: { onSearch: (query: string) => void }) {
10 const [query, setQuery] = useState('');
11 const onSearchRef = useLatest(onSearch);
12
13 useEffect(() => {
14 const timeoutId = setTimeout(() => {
15 onSearchRef.current(query);
16 }, 300);
17
18 return () => clearTimeout(timeoutId);
19 }, [query]);
20
21 return (
22 <input
23 value={query}
24 onChange={e => setQuery(e.target.value)}
25 />
26 );
27}DOM Manipulation#
1function ScrollContainer() {
2 const containerRef = useRef<HTMLDivElement>(null);
3
4 const scrollToTop = () => {
5 containerRef.current?.scrollTo({
6 top: 0,
7 behavior: 'smooth',
8 });
9 };
10
11 const scrollToBottom = () => {
12 if (containerRef.current) {
13 containerRef.current.scrollTop = containerRef.current.scrollHeight;
14 }
15 };
16
17 return (
18 <div>
19 <div ref={containerRef} className="scroll-container">
20 {/* Content */}
21 </div>
22 <button onClick={scrollToTop}>Scroll to Top</button>
23 <button onClick={scrollToBottom}>Scroll to Bottom</button>
24 </div>
25 );
26}
27
28// Focus management
29function FocusManager() {
30 const firstRef = useRef<HTMLButtonElement>(null);
31 const lastRef = useRef<HTMLButtonElement>(null);
32
33 const handleKeyDown = (e: React.KeyboardEvent) => {
34 if (e.key === 'Tab') {
35 if (e.shiftKey && document.activeElement === firstRef.current) {
36 e.preventDefault();
37 lastRef.current?.focus();
38 } else if (!e.shiftKey && document.activeElement === lastRef.current) {
39 e.preventDefault();
40 firstRef.current?.focus();
41 }
42 }
43 };
44
45 return (
46 <div onKeyDown={handleKeyDown}>
47 <button ref={firstRef}>First</button>
48 <button>Middle</button>
49 <button ref={lastRef}>Last</button>
50 </div>
51 );
52}Canvas Ref#
1function Canvas() {
2 const canvasRef = useRef<HTMLCanvasElement>(null);
3
4 useEffect(() => {
5 const canvas = canvasRef.current;
6 if (!canvas) return;
7
8 const ctx = canvas.getContext('2d');
9 if (!ctx) return;
10
11 // Draw
12 ctx.fillStyle = 'blue';
13 ctx.fillRect(10, 10, 100, 100);
14 }, []);
15
16 return <canvas ref={canvasRef} width={500} height={500} />;
17}Best Practices#
Usage:
✓ Use refs for DOM access
✓ Store mutable values that don't trigger renders
✓ Use forwardRef for reusable components
✓ Prefer controlled components when possible
Patterns:
✓ useImperativeHandle for custom API
✓ Callback refs for measurement
✓ Merge refs when needed
✓ Keep refs stable
Avoid:
✗ Overusing refs for state
✗ Directly mutating DOM in render
✗ Ignoring null checks
✗ Creating refs in loops without keys
Conclusion#
Refs enable direct DOM access and mutable value storage. Use forwardRef for component composition, useImperativeHandle for custom APIs, and callback refs for dynamic scenarios. Always handle null cases and prefer React's declarative patterns when possible.