The useRef hook is a powerful tool in React's hooks API that provides a way to create a mutable reference that persists across renders. Unlike state variables, changing a ref's value doesn't trigger a re-render, making it ideal for certain use cases that we'll explore in this lesson.
The useRef hook returns a mutable object with a .current property that is initialized to the provided argument. This object persists for the full lifetime of the component.
1const refContainer = useRef(initialValue);
The object returned by useRef has two important characteristics:
.current propertyOne of the most common uses for useRef is to access and manipulate DOM elements directly:
1import React, { useRef, useEffect } from 'react'; 2 3function AutoFocusInput() { 4 // Create a ref 5 const inputRef = useRef(null); 6 7 // Use the ref in useEffect 8 useEffect(() => { 9 // Focus the input element when component mounts 10 inputRef.current.focus(); 11 }, []); 12 13 return <input ref={inputRef} type="text" />; 14}
In this example, we:
useRef(null)ref attributeinputRef.currentfocus() method on the input elementAnother powerful use of useRef is to keep a value that persists across renders without causing re-renders when changed:
1import React, { useRef, useEffect, useState } from 'react'; 2 3function IntervalCounter() { 4 const [count, setCount] = useState(0); 5 6 // Store interval ID in a ref so it persists across renders 7 const intervalIdRef = useRef(null); 8 9 useEffect(() => { 10 // Set up the interval 11 intervalIdRef.current = setInterval(() => { 12 setCount(prevCount => prevCount + 1); 13 }, 1000); 14 15 // Clean up the interval 16 return () => { 17 clearInterval(intervalIdRef.current); 18 }; 19 }, []); // Empty dependency array means this runs once on mount 20 21 return <div>Count: {count}</div>; 22}
Here, we use a ref to store the interval ID so we can clear it when the component unmounts. Using a ref is better than a state variable because:
You can use refs to track previous values of props or state:
1import React, { useRef, useEffect, useState } from 'react'; 2 3function TrackChanges({ value }) { 4 const prevValueRef = useRef(); 5 const [hasChanged, setHasChanged] = useState(false); 6 7 useEffect(() => { 8 // Check if value has changed since last render 9 if (prevValueRef.current !== undefined && prevValueRef.current !== value) { 10 setHasChanged(true); 11 } 12 13 // Update ref to current value for next render 14 prevValueRef.current = value; 15 }, [value]); 16 17 return ( 18 <div> 19 <p>Current value: {value}</p> 20 {hasChanged && <p>The value has changed!</p>} 21 </div> 22 ); 23}
This pattern is useful when you need to compare current and previous values to perform side effects.
It's important to understand when to use useRef versus useState:
| Feature | useRef | useState |
|---|---|---|
| Triggers re-render when updated | No | Yes |
| Persists between renders | Yes | Yes |
| Access pattern | ref.current | [state, setState] |
| Best for | DOM refs, storing values without re-renders | UI state that needs to be displayed |
Use useRef when:
Use useState when:
1// Lazy initialization 2const videoRef = useRef(null); 3 4// With initial value 5const countRef = useRef(0);
1// Basic DOM access 2const imageRef = useRef(null); 3 4// Later in the code: 5if (imageRef.current) { 6 const dimensions = { 7 width: imageRef.current.clientWidth, 8 height: imageRef.current.clientHeight 9 }; 10}
1// Incorrect usage - UI won't update when count changes 2const countRef = useRef(0); 3const handleClick = () => { 4 countRef.current += 1; 5 // No re-render occurs! 6}; 7 8// Correct usage 9const [count, setCount] = useState(0); 10const handleClick = () => { 11 setCount(count + 1); 12 // Component will re-render 13};
1// Good practice 2const inputRef = useRef(null); 3 4const focusInput = () => { 5 // Check if ref is attached 6 if (inputRef.current) { 7 inputRef.current.focus(); 8 } 9};
1import React, { useRef, useState } from 'react'; 2 3function VideoPlayer() { 4 const videoRef = useRef(null); 5 const [isPlaying, setIsPlaying] = useState(false); 6 7 const handlePlayPause = () => { 8 if (isPlaying) { 9 videoRef.current.pause(); 10 } else { 11 videoRef.current.play(); 12 } 13 setIsPlaying(!isPlaying); 14 }; 15 16 return ( 17 <div> 18 <video 19 ref={videoRef} 20 src="https://example.com/video.mp4" 21 /> 22 <button onClick={handlePlayPause}> 23 {isPlaying ? 'Pause' : 'Play'} 24 </button> 25 </div> 26 ); 27}
1import React, { useRef, useEffect, useState } from 'react'; 2 3function ScrollTracker() { 4 const containerRef = useRef(null); 5 const [scrollPercentage, setScrollPercentage] = useState(0); 6 7 useEffect(() => { 8 const container = containerRef.current; 9 10 const handleScroll = () => { 11 if (container) { 12 const scrollTop = container.scrollTop; 13 const scrollHeight = container.scrollHeight - container.clientHeight; 14 const percentage = (scrollTop / scrollHeight) * 100; 15 setScrollPercentage(Math.round(percentage)); 16 } 17 }; 18 19 container.addEventListener('scroll', handleScroll); 20 return () => { 21 container.removeEventListener('scroll', handleScroll); 22 }; 23 }, []); 24 25 return ( 26 <div> 27 <div 28 ref={containerRef} 29 style={{ 30 height: '300px', 31 overflow: 'auto', 32 border: '1px solid #ccc' 33 }} 34 > 35 {/* Content here */} 36 <div style={{ height: '1000px' }}> 37 Scroll down to see the percentage change 38 </div> 39 </div> 40 <p>Scroll position: {scrollPercentage}%</p> 41 </div> 42 ); 43}
While useRef is powerful, it's not always the right tool:
Don't use it to bypass React's data flow
Don't use it for values that affect rendering
Don't overuse imperative code
For more dynamic ref assignment, you can use a callback instead of the ref attribute:
1function MeasureExample() { 2 const [height, setHeight] = useState(0); 3 4 const measuredRef = useCallback(node => { 5 if (node !== null) { 6 setHeight(node.getBoundingClientRect().height); 7 } 8 }, []); 9 10 return ( 11 <> 12 <h1 ref={measuredRef}>Hello, world</h1> 13 <p>The above header is {Math.round(height)}px tall</p> 14 </> 15 ); 16}
When creating component abstractions, you can forward refs to inner elements:
1const FancyButton = React.forwardRef((props, ref) => ( 2 <button ref={ref} className="fancy-button" {...props}> 3 {props.children} 4 </button> 5)); 6 7// Usage 8function App() { 9 const buttonRef = useRef(null); 10 11 const handleClick = () => { 12 buttonRef.current.focus(); 13 }; 14 15 return ( 16 <> 17 <FancyButton ref={buttonRef}>Click me!</FancyButton> 18 <button onClick={handleClick}>Focus the fancy button</button> 19 </> 20 ); 21}
The useRef hook is a versatile tool in the React hooks arsenal. It provides a way to:
By understanding when and how to use useRef, you can build more efficient React components and solve problems that would be difficult with state alone. Remember that while imperative code has its place, React is fundamentally a declarative library, so use useRef judiciously and preferably for cases where a declarative solution doesn't exist or would be overly complex.