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.current
focus()
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.