Need to directly grab DOM elements or keep track of values without causing your component to re-render? React’s useRef hook is your answer. While React normally manages updates through its virtual DOM, useRef gives you a way to work outside this system when needed.
Getting Started with useRef
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| import React, { useRef } from 'react';
function InputForm() {
// Create a useRef
const inputRef = useRef(null);
const focusInput = () => {
// Directly access the DOM element
inputRef.current.focus();
};
return (
<div>
<input ref={inputRef} type="text" />
<button onClick={focusInput}>Focus Input</button>
</div>
);
}
|
What is useRef?
Think of useRef as a remote control for a DOM element. Just like a TV remote gives you direct control over your television regardless of what’s playing, useRef gives you direct access to DOM elements regardless of React’s rendering cycles. The remote stays in your hand (persists between renders) and lets you directly control the element without going through React’s usual update process.
Using useRef in 3 Simple Steps
- Create your reference: Call
useRef(null)
to create a new ref - Connect to an element: Attach this ref to a DOM element using the ref attribute
- Access the element: Use the
.current
property to interact with the element
In our example, clicking the button focuses the text input - showing how useRef lets you control a DOM element directly without triggering any re-renders.
What Makes useRef Special
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
| import React, { useRef, useState, useEffect } from 'react';
function RenderCounter() {
// Regular variable - resets on every render
let regularVar = 0;
// useState - changes trigger re-renders
const [stateValue, setStateValue] = useState(0);
// useRef - persists across renders, changes don't trigger re-renders
const renderCount = useRef(0);
useEffect(() => {
regularVar++;
renderCount.current++;
console.log(`regularVar: ${regularVar}, renderCount: ${renderCount.current}`);
});
return (
<div>
<p>Render count: {renderCount.current}</p>
<p>State value: {stateValue}</p>
<button onClick={() => setStateValue(stateValue + 1)}>Increase State</button>
</div>
);
}
|
- Persistent Memory - Unlike regular variables that reset every time your component renders, useRef values stick around.
- Silent Updates - When you change a useState value, React re-renders your component. Change a useRef value, and nothing happens on screen.
- Direct DOM Access - useRef gives you a direct line to the actual DOM element, bypassing React’s usual update system.
Real-World useRef Examples
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
| function ProductList() {
const lastItemRef = useRef(null);
const [products, setProducts] = useState([]);
const [page, setPage] = useState(1);
useEffect(() => {
const observer = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting) {
// Load next page when last item is visible
setPage(prevPage => prevPage + 1);
}
});
if (lastItemRef.current) {
observer.observe(lastItemRef.current);
}
return () => observer.disconnect();
}, [products]);
return (
<div>
{products.map((product, index) => (
<div key={product.id}>
{/* Connect ref to last item */}
<div ref={index === products.length - 1 ? lastItemRef : null}>
{product.name}
</div>
</div>
))}
</div>
);
}
|
This example creates that smooth infinite scrolling you see on social media feeds. By combining useRef with IntersectionObserver, we can detect when a user reaches the bottom of your content and automatically load more items.
2. Creating a Custom Video Player
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
| function VideoPlayer() {
const videoRef = useRef(null);
const [isPlaying, setIsPlaying] = useState(false);
const togglePlay = () => {
if (videoRef.current) {
if (isPlaying) {
videoRef.current.pause();
} else {
videoRef.current.play();
}
setIsPlaying(!isPlaying);
}
};
return (
<div className="video-container">
<video
ref={videoRef}
src="/video-path.mp4"
onEnded={() => setIsPlaying(false)}
/>
<button onClick={togglePlay}>
{isPlaying ? 'Pause' : 'Play'}
</button>
</div>
);
}
|
Want to build your own video player? By connecting useRef to a video element, you can tap into all the built-in video methods like play() and pause() with clean, simple code.
When to Use useState vs useRef
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
| function StateComparisonComponent() {
// Visible values - use useState
const [inputValue, setInputValue] = useState("");
const [isSubmitted, setIsSubmitted] = useState(false);
// Internal values not shown on screen - use useRef
const submitCountRef = useRef(0);
const timerRef = useRef(null);
const handleSubmit = (e) => {
e.preventDefault();
setIsSubmitted(true);
submitCountRef.current += 1;
// Hide notification after 3 seconds
clearTimeout(timerRef.current);
timerRef.current = setTimeout(() => {
setIsSubmitted(false);
}, 3000);
};
return (
<form onSubmit={handleSubmit}>
<input
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
/>
<button type="submit">Submit</button>
{isSubmitted && <p>Submitted successfully!</p>}
</form>
);
}
|
- The Golden Rule - If users need to see it on screen, use useState. If it’s just for internal tracking, use useRef.
- Performance Benefits - Every useState update triggers a re-render of your component. useRef updates happen silently, making your app faster.
useRef Tips and Common Mistakes
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| function WarningsComponent() {
const refValue = useRef(0);
const [stateValue, setStateValue] = useState(0);
// WRONG: Changing .current during rendering
refValue.current += 1; // Don't do this!
const handleIncrement = () => {
// CORRECT: Change .current in event handlers
refValue.current += 1;
console.log(refValue.current);
// Update state to display the value on screen
setStateValue(refValue.current);
};
return (
<div>
<p>Ref value: {refValue.current}</p>
<p>State value: {stateValue}</p>
<button onClick={handleIncrement}>Increment</button>
</div>
);
}
|
- Don’t Change Refs During Rendering - Never modify ref values in the main component body - only in event handlers or effects.
- Screen Updates Need State - Changing a ref value won’t update your UI. To show a ref value, sync it to a state value.
- Safety Check First - Always check that your ref exists before using it:
if (myRef.current) { /* now it's safe */ }
Wrap-Up
The useRef hook gives you a direct line to DOM elements and a place to store values that persist between renders without causing unnecessary updates. It’s like having a secret toolbox that works alongside React’s main system.
Key takeaways:
- Use useState for anything visible on screen
- Use useRef for behind-the-scenes tracking
- Only change ref values in event handlers or effects
- Always check that ref elements exist before using them