Ref callbacks provide more control than useRef for dynamic scenarios. Here's how to use them.
Basic Ref Callback#
1import { useCallback, useState } from 'react';
2
3function MeasuredComponent() {
4 const [height, setHeight] = useState(0);
5
6 // Ref callback receives the DOM element
7 const measuredRef = useCallback((node: HTMLDivElement | null) => {
8 if (node !== null) {
9 setHeight(node.getBoundingClientRect().height);
10 }
11 }, []);
12
13 return (
14 <div ref={measuredRef}>
15 <p>This element is {height}px tall</p>
16 <p>More content here...</p>
17 </div>
18 );
19}
20
21// Without useCallback (simpler but recreates on each render)
22function SimpleRef() {
23 const [width, setWidth] = useState(0);
24
25 return (
26 <div ref={(node) => {
27 if (node) {
28 setWidth(node.offsetWidth);
29 }
30 }}>
31 Width: {width}px
32 </div>
33 );
34}Dynamic Element Lists#
1import { useRef, useCallback } from 'react';
2
3function DynamicList({ items }) {
4 // Store refs for multiple elements
5 const itemRefs = useRef<Map<string, HTMLLIElement>>(new Map());
6
7 const setItemRef = useCallback((id: string) => {
8 return (node: HTMLLIElement | null) => {
9 if (node) {
10 itemRefs.current.set(id, node);
11 } else {
12 itemRefs.current.delete(id);
13 }
14 };
15 }, []);
16
17 const scrollToItem = (id: string) => {
18 const node = itemRefs.current.get(id);
19 node?.scrollIntoView({ behavior: 'smooth' });
20 };
21
22 return (
23 <ul>
24 {items.map((item) => (
25 <li key={item.id} ref={setItemRef(item.id)}>
26 {item.name}
27 </li>
28 ))}
29 </ul>
30 );
31}Focus Management#
1function AutoFocusInput() {
2 const focusRef = useCallback((node: HTMLInputElement | null) => {
3 if (node) {
4 node.focus();
5 node.select(); // Also select text
6 }
7 }, []);
8
9 return <input ref={focusRef} defaultValue="Edit me" />;
10}
11
12// Focus first invalid field
13function Form() {
14 const firstErrorRef = useCallback((node: HTMLInputElement | null) => {
15 if (node) {
16 node.focus();
17 node.scrollIntoView({ behavior: 'smooth', block: 'center' });
18 }
19 }, []);
20
21 return (
22 <form>
23 <input name="name" />
24 <input
25 name="email"
26 ref={hasError ? firstErrorRef : undefined}
27 />
28 <input name="phone" />
29 </form>
30 );
31}Intersection Observer#
1import { useCallback, useState } from 'react';
2
3function LazyImage({ src, alt }) {
4 const [isVisible, setIsVisible] = useState(false);
5
6 const imageRef = useCallback((node: HTMLImageElement | null) => {
7 if (!node) return;
8
9 const observer = new IntersectionObserver(
10 ([entry]) => {
11 if (entry.isIntersecting) {
12 setIsVisible(true);
13 observer.disconnect();
14 }
15 },
16 { rootMargin: '100px' }
17 );
18
19 observer.observe(node);
20
21 // Cleanup handled by disconnect on intersection
22 }, []);
23
24 return (
25 <img
26 ref={imageRef}
27 src={isVisible ? src : '/placeholder.png'}
28 alt={alt}
29 />
30 );
31}
32
33// With cleanup using useEffect
34function ObservedElement({ onVisible }) {
35 const [element, setElement] = useState<HTMLDivElement | null>(null);
36
37 useEffect(() => {
38 if (!element) return;
39
40 const observer = new IntersectionObserver(([entry]) => {
41 if (entry.isIntersecting) {
42 onVisible();
43 }
44 });
45
46 observer.observe(element);
47
48 return () => observer.disconnect();
49 }, [element, onVisible]);
50
51 return <div ref={setElement}>Observed content</div>;
52}ResizeObserver#
1import { useCallback, useState, useEffect } from 'react';
2
3function ResizableElement() {
4 const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
5 const [element, setElement] = useState<HTMLDivElement | null>(null);
6
7 useEffect(() => {
8 if (!element) return;
9
10 const observer = new ResizeObserver((entries) => {
11 const { width, height } = entries[0].contentRect;
12 setDimensions({ width, height });
13 });
14
15 observer.observe(element);
16
17 return () => observer.disconnect();
18 }, [element]);
19
20 return (
21 <div ref={setElement} style={{ resize: 'both', overflow: 'auto' }}>
22 <p>Width: {Math.round(dimensions.width)}px</p>
23 <p>Height: {Math.round(dimensions.height)}px</p>
24 </div>
25 );
26}
27
28// Custom hook
29function useResizeObserver<T extends HTMLElement>() {
30 const [element, setElement] = useState<T | null>(null);
31 const [size, setSize] = useState({ width: 0, height: 0 });
32
33 useEffect(() => {
34 if (!element) return;
35
36 const observer = new ResizeObserver((entries) => {
37 const { width, height } = entries[0].contentRect;
38 setSize({ width, height });
39 });
40
41 observer.observe(element);
42 return () => observer.disconnect();
43 }, [element]);
44
45 return [setElement, size] as const;
46}
47
48// Usage
49function Component() {
50 const [ref, { width, height }] = useResizeObserver<HTMLDivElement>();
51
52 return (
53 <div ref={ref}>
54 {width} x {height}
55 </div>
56 );
57}Animation Trigger#
1import { useCallback } from 'react';
2
3function AnimatedElement() {
4 const animateRef = useCallback((node: HTMLDivElement | null) => {
5 if (!node) return;
6
7 // Trigger CSS animation
8 node.classList.add('animate-in');
9
10 // Or use Web Animations API
11 node.animate(
12 [
13 { opacity: 0, transform: 'translateY(20px)' },
14 { opacity: 1, transform: 'translateY(0)' },
15 ],
16 {
17 duration: 300,
18 easing: 'ease-out',
19 fill: 'forwards',
20 }
21 );
22 }, []);
23
24 return (
25 <div ref={animateRef} className="card">
26 Animated content
27 </div>
28 );
29}
30
31// Staggered animation for list
32function AnimatedList({ items }) {
33 const createAnimatedRef = useCallback((index: number) => {
34 return (node: HTMLLIElement | null) => {
35 if (!node) return;
36
37 node.animate(
38 [
39 { opacity: 0, transform: 'translateX(-20px)' },
40 { opacity: 1, transform: 'translateX(0)' },
41 ],
42 {
43 duration: 300,
44 delay: index * 50, // Stagger
45 easing: 'ease-out',
46 fill: 'forwards',
47 }
48 );
49 };
50 }, []);
51
52 return (
53 <ul>
54 {items.map((item, index) => (
55 <li key={item.id} ref={createAnimatedRef(index)}>
56 {item.name}
57 </li>
58 ))}
59 </ul>
60 );
61}Third-Party Libraries#
1import { useCallback, useEffect, useRef } from 'react';
2
3function ChartComponent({ data }) {
4 const chartInstance = useRef<Chart | null>(null);
5
6 const chartRef = useCallback((canvas: HTMLCanvasElement | null) => {
7 if (!canvas) {
8 // Cleanup on unmount
9 chartInstance.current?.destroy();
10 chartInstance.current = null;
11 return;
12 }
13
14 // Initialize chart library
15 chartInstance.current = new Chart(canvas, {
16 type: 'line',
17 data: data,
18 });
19 }, [data]);
20
21 return <canvas ref={chartRef} />;
22}
23
24// Video player
25function VideoPlayer({ src }) {
26 const playerRef = useCallback((video: HTMLVideoElement | null) => {
27 if (!video) return;
28
29 // Initialize video player library
30 const player = new VideoJS(video, {
31 controls: true,
32 autoplay: false,
33 });
34
35 return () => player.dispose();
36 }, []);
37
38 return (
39 <video ref={playerRef} className="video-js">
40 <source src={src} type="video/mp4" />
41 </video>
42 );
43}Merging Refs#
1import { useCallback, useRef, forwardRef } from 'react';
2
3// Merge multiple refs
4function mergeRefs<T>(...refs: (React.Ref<T> | undefined)[]) {
5 return (node: T | null) => {
6 refs.forEach((ref) => {
7 if (typeof ref === 'function') {
8 ref(node);
9 } else if (ref) {
10 (ref as React.MutableRefObject<T | null>).current = node;
11 }
12 });
13 };
14}
15
16// Component with internal and forwarded ref
17const Input = forwardRef<HTMLInputElement, InputProps>((props, forwardedRef) => {
18 const internalRef = useRef<HTMLInputElement>(null);
19
20 const measureRef = useCallback((node: HTMLInputElement | null) => {
21 if (node) {
22 console.log('Input width:', node.offsetWidth);
23 }
24 }, []);
25
26 return (
27 <input
28 ref={mergeRefs(forwardedRef, internalRef, measureRef)}
29 {...props}
30 />
31 );
32});Best Practices#
Usage:
✓ Use for measurement/observers
✓ Use for dynamic element lists
✓ Use for third-party integrations
✓ Wrap in useCallback when needed
Cleanup:
✓ Handle null (element unmounted)
✓ Disconnect observers
✓ Dispose library instances
✓ Clear stored references
Performance:
✓ Memoize with useCallback
✓ Avoid creating refs in loops
✓ Use Map for dynamic refs
✓ Batch state updates
Avoid:
✗ Side effects without cleanup
✗ Recreating refs unnecessarily
✗ Forgetting null checks
✗ Complex logic in callbacks
Conclusion#
Ref callbacks provide fine-grained control for measuring elements, managing observers, and integrating third-party libraries. Use them when useRef isn't flexible enough, especially for dynamic lists and elements that need setup/cleanup logic. Always handle the null case for proper cleanup.