The useRef hook is one of React's most versatile tools. It lets you access DOM elements directly (like focusing an input), and store values that persist between renders without triggering re-renders (like tracking previous state or storing timers). While useState is for values that should trigger re-renders, useRef is for values that shouldn't. Let's explore everything useRef can do! šÆ
What Is useRef?
useRef creates a mutable object that persists across renders:
function Component() {
const ref = React.useRef(initialValue)
// ref is an object: { current: initialValue }
console.log(ref.current) // Access the value
ref.current = 'new value' // Update the value (no re-render!)
return <div>Component</div>
}
// Key points:
// 1. useRef returns an object with a 'current' property
// 2. ref.current can be read and updated
// 3. Updating ref.current does NOT trigger re-render
// 4. Ref persists across renders (same object every time)useRef vs useState:
useState:
⢠Triggers re-render when updated
⢠For UI-related data
⢠Asynchronous updates
useRef:
⢠Does NOT trigger re-render
⢠For non-UI data or DOM access
⢠Synchronous updates
Use Case 1: Accessing DOM Elements
The most common use of useRef is to access DOM elements directly:
Focusing an Input
function SearchBox() {
const inputRef = React.useRef(null)
const handleClick = () => {
// Access the actual DOM element
inputRef.current.focus()
}
return (
<div>
<input ref={inputRef} type="text" placeholder="Search..." />
<button onClick={handleClick}>Focus Input</button>
</div>
)
}
// How it works:
// 1. Create ref with useRef(null)
// 2. Pass ref to element's ref prop
// 3. After render, ref.current = DOM element
// 4. Can call DOM methods like .focus()Scrolling to Element
function ScrollDemo() {
const sectionRef = React.useRef(null)
const scrollToSection = () => {
sectionRef.current.scrollIntoView({
behavior: 'smooth'
})
}
return (
<div>
<button onClick={scrollToSection}>Scroll to Section</button>
<div style={{ height: '1000px' }}>
{/* Lots of content */}
</div>
<div ref={sectionRef}>
<h2>Important Section</h2>
<p>Content here...</p>
</div>
</div>
)
}Playing/Pausing Video
function VideoPlayer() {
const videoRef = React.useRef(null)
const play = () => {
videoRef.current.play()
}
const pause = () => {
videoRef.current.pause()
}
return (
<div>
<video ref={videoRef} src="/video.mp4" />
<button onClick={play}>Play</button>
<button onClick={pause}>Pause</button>
</div>
)
}Getting Element Dimensions
function MeasureBox() {
const boxRef = React.useRef(null)
const [dimensions, setDimensions] = React.useState({ width: 0, height: 0 })
const measure = () => {
const box = boxRef.current
setDimensions({
width: box.offsetWidth,
height: box.offsetHeight
})
}
return (
<div>
<div
ref={boxRef}
style={{ width: '200px', height: '100px', background: 'lightblue' }}
>
Box to measure
</div>
<button onClick={measure}>Measure</button>
<p>Width: {dimensions.width}px</p>
<p>Height: {dimensions.height}px</p>
</div>
)
}ā DOM Access Best Practice
Use refs for imperative DOM operations:
⢠Focusing inputs
⢠Scrolling
⢠Playing media
⢠Measuring elements
⢠Integrating with non-React libraries
Don't use refs for things React can handle declaratively!
Use Case 2: Storing Mutable Values
Use refs to store values that shouldn't trigger re-renders:
Storing Previous State
function Counter() {
const [count, setCount] = React.useState(0)
const prevCountRef = React.useRef(0)
React.useEffect(() => {
// Store current count after render
prevCountRef.current = count
})
return (
<div>
<p>Current count: {count}</p>
<p>Previous count: {prevCountRef.current}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
)
}
// How it works:
// 1. Component renders with count = 0, prevCountRef = 0
// 2. useEffect runs, sets prevCountRef = 0
// 3. User clicks, count becomes 1
// 4. Component renders with count = 1, prevCountRef still = 0 (shows previous!)
// 5. useEffect runs, sets prevCountRef = 1Storing Timer IDs
function Stopwatch() {
const [time, setTime] = React.useState(0)
const [isRunning, setIsRunning] = React.useState(false)
const intervalRef = React.useRef(null)
const start = () => {
setIsRunning(true)
// Store interval ID in ref
intervalRef.current = setInterval(() => {
setTime(t => t + 1)
}, 1000)
}
const stop = () => {
setIsRunning(false)
// Use ref to clear interval
clearInterval(intervalRef.current)
}
const reset = () => {
stop()
setTime(0)
}
return (
<div>
<p>Time: {time}s</p>
{!isRunning ? (
<button onClick={start}>Start</button>
) : (
<button onClick={stop}>Stop</button>
)}
<button onClick={reset}>Reset</button>
</div>
)
}
// Why use ref for interval ID?
// - Don't want re-render when storing ID
// - Need to access ID later to clear interval
// - Perfect use case for useRef!Tracking Render Count
function Component() {
const [name, setName] = React.useState('')
const renderCount = React.useRef(0)
// Increment on every render
renderCount.current = renderCount.current + 1
return (
<div>
<p>This component has rendered {renderCount.current} times</p>
<input
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Type to trigger re-renders"
/>
</div>
)
}
// Why this works:
// - renderCount.current updates don't trigger re-renders
// - Value persists across renders
// - Can see how many times component has rendered!Caching Expensive Calculations
function DataProcessor({ data }) {
const cacheRef = React.useRef({})
const processData = (input) => {
// Check cache
if (cacheRef.current[input]) {
console.log('Using cached result')
return cacheRef.current[input]
}
// Expensive calculation
console.log('Computing...')
const result = expensiveOperation(input)
// Store in cache
cacheRef.current[input] = result
return result
}
const result = processData(data)
return <div>Result: {result}</div>
}
// Ref stores cache across renders without triggering re-renders
// Note: Usually useMemo is better for this, but this shows the concept!When to Use Ref vs State
Use State When:
- Value is displayed in UI
- Changing value should trigger re-render
- Value affects what's rendered
// ā
Use state - value is displayed
function Counter() {
const [count, setCount] = React.useState(0)
return (
<div>
<p>Count: {count}</p> {/* Displayed in UI */}
<button onClick={() => setCount(count + 1)}>+</button>
</div>
)
}Use Ref When:
- Accessing DOM elements
- Storing values that shouldn't trigger re-render
- Keeping track of previous values
- Storing timer/subscription IDs
// ā
Use ref - just storing ID, not displaying
function Timer() {
const [time, setTime] = React.useState(0)
const intervalRef = React.useRef(null) // Just storing ID
const start = () => {
intervalRef.current = setInterval(() => {
setTime(t => t + 1)
}, 1000)
}
return (
<div>
<p>Time: {time}</p> {/* State for displayed value */}
<button onClick={start}>Start</button>
</div>
)
}Common Mistake: Using Ref for UI Data
ā Don't do this:ref.current = newValue (and expect UI to update)
Updating ref.current doesn't trigger re-render!
ā
Use state instead:setState(newValue)
Interactive Demo
š” Tip: Edit the code above and click "Run" to see your changes
Common useRef Patterns
Pattern 1: Custom Input Hook
function useAutoFocus() {
const inputRef = React.useRef(null)
React.useEffect(() => {
inputRef.current.focus()
}, [])
return inputRef
}
// Usage:
function LoginForm() {
const emailRef = useAutoFocus()
return <input ref={emailRef} type="email" />
}
// Email input automatically focused on mount!Pattern 2: Previous Value Hook
function usePrevious(value) {
const ref = React.useRef()
React.useEffect(() => {
ref.current = value
})
return ref.current
}
// Usage:
function Counter() {
const [count, setCount] = React.useState(0)
const prevCount = usePrevious(count)
return (
<div>
<p>Current: {count}</p>
<p>Previous: {prevCount}</p>
<button onClick={() => setCount(count + 1)}>+</button>
</div>
)
}Pattern 3: Debounce with Ref
function SearchBox() {
const [query, setQuery] = React.useState('')
const [results, setResults] = React.useState([])
const timeoutRef = React.useRef(null)
const handleChange = (e) => {
const value = e.target.value
setQuery(value)
// Clear previous timeout
clearTimeout(timeoutRef.current)
// Set new timeout
timeoutRef.current = setTimeout(() => {
// Search after 500ms delay
fetch(`/api/search?q=${value}`)
.then(res => res.json())
.then(data => setResults(data))
}, 500)
}
return (
<div>
<input value={query} onChange={handleChange} />
<ul>
{results.map(item => <li key={item.id}>{item.name}</li>)}
</ul>
</div>
)
}Common useRef Mistakes
1. Expecting Re-render on Update
// ā Wrong - UI won't update
function Counter() {
const countRef = React.useRef(0)
const increment = () => {
countRef.current += 1
// Component won't re-render!
}
return (
<div>
<p>Count: {countRef.current}</p> {/* Won't update! */}
<button onClick={increment}>+</button>
</div>
)
}
// ā
Correct - Use state for UI values
function Counter() {
const [count, setCount] = React.useState(0)
return (
<div>
<p>Count: {count}</p> {/* Updates correctly! */}
<button onClick={() => setCount(count + 1)}>+</button>
</div>
)
}2. Accessing Ref During Render
// ā Wrong - Don't read/write during render
function Component() {
const ref = React.useRef(0)
ref.current += 1 // Bad! Side effect during render
return <div>{ref.current}</div>
}
// ā
Correct - Use in event handler or effect
function Component() {
const ref = React.useRef(0)
React.useEffect(() => {
ref.current += 1 // Good! In effect
})
const handleClick = () => {
ref.current += 1 // Good! In event handler
}
return <button onClick={handleClick}>Click</button>
}3. Forgetting .current
// ā Wrong - Ref IS the object, not the value
function Component() {
const inputRef = React.useRef(null)
const focus = () => {
inputRef.focus() // Error! inputRef is object, not element
}
return <input ref={inputRef} />
}
// ā
Correct - Use .current to access value
function Component() {
const inputRef = React.useRef(null)
const focus = () => {
inputRef.current.focus() // Correct!
}
return <input ref={inputRef} />
}Key Takeaways
useRefcreates a mutable object that persists across renders- Ref object has a
currentproperty you can read/write - Updating
ref.currentdoes NOT trigger re-render - Use Case 1: Accessing DOM elements (focus, scroll, measure)
- Use Case 2: Storing values that shouldn't trigger re-renders
- Pass ref to element's
refprop to access DOM - Use state for UI values, use ref for non-UI values
- Common uses: timers, previous values, render counts, caching
- Don't read/write refs during render (use effects or event handlers)
- Refs are perfect for integrating with non-React libraries
- Always access ref value with
.current - Refs persist the same object identity across renders
What's Next?
You now understand useRef and when to use it! You know how to access DOM elements and store values that don't trigger re-renders.
In the next lesson, you'll learn about useContext Hook:
- What is Context and why use it
- Creating and providing context
- Consuming context with useContext
- Avoiding prop drilling
- Common context patterns
Get ready to share state across your entire component tree without passing props! š³