Back to Blog
ReactHooksuseRefDOM

React useRef Guide

Master React useRef hook for DOM references, mutable values, and previous state.

B
Bootspring Team
Engineering
September 17, 2018
6 min read

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.

Share this article

Help spread the word about Bootspring