useRef provides mutable references that persist across renders. Here's how to use it effectively.
Basic useRef#
1import { useRef } from 'react';
2
3function TextInput() {
4 // Create a ref
5 const inputRef = useRef(null);
6
7 const focusInput = () => {
8 // Access DOM element
9 inputRef.current.focus();
10 };
11
12 return (
13 <div>
14 <input ref={inputRef} type="text" />
15 <button onClick={focusInput}>Focus Input</button>
16 </div>
17 );
18}DOM References#
1function VideoPlayer() {
2 const videoRef = useRef(null);
3
4 const play = () => videoRef.current.play();
5 const pause = () => videoRef.current.pause();
6
7 return (
8 <div>
9 <video ref={videoRef} src="video.mp4" />
10 <button onClick={play}>Play</button>
11 <button onClick={pause}>Pause</button>
12 </div>
13 );
14}
15
16// Multiple refs
17function Form() {
18 const nameRef = useRef(null);
19 const emailRef = useRef(null);
20
21 const handleSubmit = () => {
22 console.log('Name:', nameRef.current.value);
23 console.log('Email:', emailRef.current.value);
24 };
25
26 return (
27 <form onSubmit={handleSubmit}>
28 <input ref={nameRef} name="name" />
29 <input ref={emailRef} name="email" type="email" />
30 <button type="submit">Submit</button>
31 </form>
32 );
33}Mutable Values#
1function Timer() {
2 const [count, setCount] = useState(0);
3 const intervalRef = useRef(null);
4
5 const start = () => {
6 if (intervalRef.current) return;
7
8 intervalRef.current = setInterval(() => {
9 setCount(c => c + 1);
10 }, 1000);
11 };
12
13 const stop = () => {
14 clearInterval(intervalRef.current);
15 intervalRef.current = null;
16 };
17
18 // Cleanup on unmount
19 useEffect(() => {
20 return () => clearInterval(intervalRef.current);
21 }, []);
22
23 return (
24 <div>
25 <p>Count: {count}</p>
26 <button onClick={start}>Start</button>
27 <button onClick={stop}>Stop</button>
28 </div>
29 );
30}Previous Value Pattern#
1function usePrevious(value) {
2 const ref = useRef();
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}</p>
19 <button onClick={() => setCount(c => c + 1)}>Increment</button>
20 </div>
21 );
22}Callback Refs#
1function MeasuredComponent() {
2 const [height, setHeight] = useState(0);
3
4 // Callback ref - called when element mounts/unmounts
5 const measuredRef = useCallback(node => {
6 if (node !== null) {
7 setHeight(node.getBoundingClientRect().height);
8 }
9 }, []);
10
11 return (
12 <div>
13 <div ref={measuredRef}>
14 Content that might change size
15 </div>
16 <p>Height: {height}px</p>
17 </div>
18 );
19}
20
21// With ResizeObserver
22function ResizableComponent() {
23 const [size, setSize] = useState({ width: 0, height: 0 });
24 const observerRef = useRef(null);
25
26 const ref = useCallback(node => {
27 if (observerRef.current) {
28 observerRef.current.disconnect();
29 }
30
31 if (node) {
32 observerRef.current = new ResizeObserver(entries => {
33 const { width, height } = entries[0].contentRect;
34 setSize({ width, height });
35 });
36 observerRef.current.observe(node);
37 }
38 }, []);
39
40 useEffect(() => {
41 return () => observerRef.current?.disconnect();
42 }, []);
43
44 return (
45 <div ref={ref}>
46 Width: {size.width}, Height: {size.height}
47 </div>
48 );
49}Forwarding Refs#
1import { forwardRef, useRef } from 'react';
2
3// Forward ref to child component
4const FancyInput = forwardRef((props, ref) => {
5 return <input ref={ref} className="fancy" {...props} />;
6});
7
8function Parent() {
9 const inputRef = useRef(null);
10
11 return (
12 <div>
13 <FancyInput ref={inputRef} />
14 <button onClick={() => inputRef.current.focus()}>
15 Focus
16 </button>
17 </div>
18 );
19}
20
21// With useImperativeHandle
22const CustomInput = forwardRef((props, ref) => {
23 const inputRef = useRef(null);
24
25 useImperativeHandle(ref, () => ({
26 focus: () => inputRef.current.focus(),
27 clear: () => { inputRef.current.value = ''; },
28 getValue: () => inputRef.current.value,
29 }));
30
31 return <input ref={inputRef} {...props} />;
32});
33
34function Parent() {
35 const inputRef = useRef(null);
36
37 return (
38 <div>
39 <CustomInput ref={inputRef} />
40 <button onClick={() => inputRef.current.focus()}>Focus</button>
41 <button onClick={() => inputRef.current.clear()}>Clear</button>
42 </div>
43 );
44}Avoiding Re-renders#
1// useRef doesn't trigger re-renders
2function ClickTracker() {
3 const clickCount = useRef(0);
4
5 const handleClick = () => {
6 clickCount.current += 1;
7 console.log(`Clicked ${clickCount.current} times`);
8 // No re-render!
9 };
10
11 return <button onClick={handleClick}>Click me</button>;
12}
13
14// Compare with useState
15function ClickTrackerWithState() {
16 const [clickCount, setClickCount] = useState(0);
17
18 const handleClick = () => {
19 setClickCount(c => c + 1); // Causes re-render
20 };
21
22 return <button onClick={handleClick}>Clicked {clickCount}</button>;
23}
24
25// Use ref for values that don't affect UI
26function Analytics() {
27 const sessionData = useRef({
28 pageViews: 0,
29 clicks: 0,
30 startTime: Date.now()
31 });
32
33 const trackClick = () => {
34 sessionData.current.clicks++;
35 // Update analytics without re-render
36 };
37
38 return <button onClick={trackClick}>Track</button>;
39}Latest Value Pattern#
1// Access latest value in callbacks
2function Chat({ messages, onSend }) {
3 const messagesRef = useRef(messages);
4 messagesRef.current = messages;
5
6 useEffect(() => {
7 const interval = setInterval(() => {
8 // Always has latest messages
9 console.log('Current messages:', messagesRef.current.length);
10 }, 5000);
11
12 return () => clearInterval(interval);
13 }, []); // Empty deps, but still gets latest value
14
15 return /* ... */;
16}
17
18// Latest callback pattern
19function useLatestCallback(callback) {
20 const ref = useRef(callback);
21 ref.current = callback;
22
23 return useCallback((...args) => {
24 return ref.current(...args);
25 }, []);
26}
27
28function Search({ onSearch }) {
29 const latestOnSearch = useLatestCallback(onSearch);
30
31 useEffect(() => {
32 // Safe to use in effect without adding to deps
33 latestOnSearch('initial');
34 }, [latestOnSearch]);
35}Animation Refs#
1function AnimatedComponent() {
2 const elementRef = useRef(null);
3 const animationRef = useRef(null);
4
5 const startAnimation = () => {
6 animationRef.current = elementRef.current.animate(
7 [
8 { transform: 'translateX(0)' },
9 { transform: 'translateX(100px)' }
10 ],
11 { duration: 1000, fill: 'forwards' }
12 );
13 };
14
15 const stopAnimation = () => {
16 animationRef.current?.cancel();
17 };
18
19 return (
20 <div>
21 <div ref={elementRef} className="box" />
22 <button onClick={startAnimation}>Animate</button>
23 <button onClick={stopAnimation}>Stop</button>
24 </div>
25 );
26}Scroll Management#
1function ScrollToBottom() {
2 const bottomRef = useRef(null);
3 const containerRef = useRef(null);
4
5 const scrollToBottom = () => {
6 bottomRef.current?.scrollIntoView({ behavior: 'smooth' });
7 };
8
9 // Auto-scroll on new messages
10 useEffect(() => {
11 scrollToBottom();
12 }, [messages]);
13
14 return (
15 <div ref={containerRef} className="chat">
16 {messages.map(msg => <Message key={msg.id} {...msg} />)}
17 <div ref={bottomRef} />
18 </div>
19 );
20}
21
22// Save and restore scroll position
23function ScrollRestoration({ items }) {
24 const containerRef = useRef(null);
25 const scrollPosRef = useRef(0);
26
27 const saveScroll = () => {
28 scrollPosRef.current = containerRef.current.scrollTop;
29 };
30
31 const restoreScroll = () => {
32 containerRef.current.scrollTop = scrollPosRef.current;
33 };
34
35 return (
36 <div
37 ref={containerRef}
38 onScroll={saveScroll}
39 >
40 {items.map(/* ... */)}
41 </div>
42 );
43}TypeScript#
1// Typed refs
2const inputRef = useRef<HTMLInputElement>(null);
3const divRef = useRef<HTMLDivElement>(null);
4
5// Mutable ref with initial value
6const countRef = useRef<number>(0);
7countRef.current = 5; // OK
8
9// Nullable ref
10const elementRef = useRef<HTMLElement | null>(null);
11
12// Forward ref with types
13interface InputProps {
14 label: string;
15}
16
17const Input = forwardRef<HTMLInputElement, InputProps>(
18 ({ label }, ref) => (
19 <label>
20 {label}
21 <input ref={ref} />
22 </label>
23 )
24);Best Practices#
Use ref for:
✓ DOM element access
✓ Mutable values without re-render
✓ Timer/animation IDs
✓ Previous values
Patterns:
✓ useCallback for measurement refs
✓ forwardRef for component refs
✓ useImperativeHandle for custom API
✓ Cleanup in useEffect
Avoid:
✗ Reading ref.current during render
✗ Using ref for render-dependent data
✗ Overusing refs instead of state
✗ Forgetting null checks
Performance:
✓ Track values without re-renders
✓ Store computed values
✓ Cache DOM measurements
✓ Hold instance variables
Conclusion#
useRef provides persistent mutable values across renders. Use it for DOM access, storing values that don't affect rendering, tracking previous state, and holding timer/animation IDs. Unlike state, updating refs doesn't trigger re-renders. Remember that refs are null initially when used for DOM elements, so always check before accessing.