React useRef Hook — Complete Guide with Examples
In this tutorial, you'll learn about React useRef Hook. We cover key concepts, practical examples, and best practices.
The React useRef hook creates a mutable reference that persists across renders without causing re-renders when its value changes, making it essential for DOM access and storing non-reactive data.
What You'll Learn
Master the useRef hook — access DOM elements directly, store mutable values that survive re-renders, track render counts, and understand when to use refs instead of state.
Why It Matters
Every React developer hits a wall: "How do I focus an input?" or "How do I store a timer ID without causing re-renders?" useRef solves these. It's the tool for values that need to persist but don't need to trigger UI updates.
Real-World Use
Focusing a search input on page load, storing a WebSocket connection reference, tracking how many times a component rendered, or holding an interval ID for a stopwatch.
What is useRef?
The useRef hook returns a mutable object with a .current property. Unlike state, updating a ref does not trigger a re-render. The ref object lives for the entire lifetime of the component.
import { useRef } from "react";
function SimpleExample() {
// Ref starts as { current: null }
const ref = useRef(null);
console.log(ref); // { current: null }
return <p>Check the console</p>;
}
Think of a ref as a drawer built into your component. You can put things in the drawer, take them out, or swap them — but the drawer itself doesn't make any noise. Nobody knows you changed what's inside. This is different from state, which is like a loudspeaker — every change announces itself and causes a re-render.
Accessing DOM Elements
The most common use case for useRef is getting direct access to a DOM element. Instead of using document.getElementById, you attach a ref to a JSX element.
import { useRef, useEffect } from "react";
function AutoFocusInput() {
const inputRef = useRef(null);
useEffect(() => {
// Automatically focus the input when component mounts
inputRef.current.focus();
}, []);
return (
<div style={{ padding: "20px" }}>
<input
ref={inputRef}
type="text"
placeholder="I get auto-focused!"
style={{ padding: "8px", fontSize: "16px" }}
/>
</div>
);
}
How it works:
useRef(null)creates a ref with initial value{ current: null }.<input ref={inputRef} />— React assigns the real DOM node toinputRef.current.- Inside
useEffect,inputRef.current.focus()calls the native DOM focus method. - We use
useEffecthere because the DOM element doesn't exist until after the component mounts.
Expected output: When this component renders, the input field automatically receives focus (cursor appears inside it).
Storing Mutable Values
Refs are perfect for values that change but should not cause re-renders. A classic example is storing a timer ID.
import { useState, useRef } from "react";
function Stopwatch() {
const [count, setCount] = useState(0);
const intervalRef = useRef(null);
function start() {
if (intervalRef.current !== null) return; // Already running
intervalRef.current = setInterval(() => {
setCount(prev => prev + 1);
}, 1000);
}
function stop() {
if (intervalRef.current !== null) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
}
function reset() {
stop();
setCount(0);
}
return (
<div style={{ padding: "20px" }}>
<h2>Stopwatch: {count}s</h2>
<button onClick={start} style={{ marginRight: "8px" }}>Start</button>
<button onClick={stop} style={{ marginRight: "8px" }}>Stop</button>
<button onClick={reset}>Reset</button>
</div>
);
}
Why not use state for the interval ID? Because updating state causes a re-render. The interval ID is just a number we need to store — it's not UI data. Changing it shouldn't flash the screen. A ref stores it silently.
Tracking Renders
You can use a ref to count how many times a component renders without causing infinite loops.
import { useState, useRef, useEffect } from "react";
function RenderCounter() {
const [text, setText] = useState("");
const renderCount = useRef(0);
// Increment ref on every render
renderCount.current += 1;
return (
<div style={{ padding: "20px" }}>
<input
value={text}
onChange={e => setText(e.target.value)}
placeholder="Type something..."
style={{ padding: "8px", fontSize: "16px" }}
/>
<p>You typed: {text}</p>
<p style={{ color: "#666" }}>
Component rendered {renderCount.current} times
</p>
</div>
);
}
Expected output: Every time you type in the input, the render counter increments. This works because setting state (via setText) triggers a re-render, and the ref keeps counting. If we used useState for the render count, we'd get an infinite loop — updating state would trigger a re-render, which would update state again, and so on.
useRef vs useState
| Aspect | useRef | useState |
|---|---|---|
| Triggers re-render | No | Yes |
| Persists across renders | Yes | Yes |
| Syntax | ref.current = value |
setState(value) |
| Reading value | ref.current (synchronous) |
State value (available next render) |
| Best for | DOM refs, timers, mutable data | UI data, user input |
| Initial value | useRef(initial) |
useState(initial) |
What is the difference between useRef and useState?
useRef persists values across renders without causing re-renders when the value changes, making it ideal for DOM references and non-UI data. useState triggers a re-render every time the value updates, which is necessary for data displayed on screen.
Previous State with useRef
You can combine useState and useRef to track the previous value of a state variable — useful for comparisons.
import { useState, useRef, useEffect } from "react";
function PreviousValueTracker() {
const [count, setCount] = useState(0);
const prevCountRef = useRef(null);
useEffect(() => {
prevCountRef.current = count;
}, [count]);
const prevCount = prevCountRef.current;
return (
<div style={{ padding: "20px" }}>
<h2>Current: {count}</h2>
<h2 style={{ color: "#666" }}>
Previous: {prevCount !== null ? prevCount : "N/A"}
</h2>
<button onClick={() => setCount(c => c + 1)}>Increment</button>
</div>
);
}
How it works: The useEffect runs after the render, so prevCountRef.current always holds the value from the previous render cycle. On the first render, it's null.
useRef for Video and Media
Refs shine when controlling media elements like video, audio, or canvas.
import { useRef, useState } from "react";
function VideoPlayer({ src }) {
const videoRef = useRef(null);
const [isPlaying, setIsPlaying] = useState(false);
function togglePlay() {
const video = videoRef.current;
if (video.paused) {
video.play();
setIsPlaying(true);
} else {
video.pause();
setIsPlaying(false);
}
}
return (
<div style={{ padding: "20px" }}>
<video
ref={videoRef}
src={src}
width="400"
style={{ display: "block", marginBottom: "10px" }}
/>
<button onClick={togglePlay}>
{isPlaying ? "Pause" : "Play"}
</button>
</div>
);
}
This pattern is used in media players, video conferencing apps, and security camera interfaces built by tools like Doda Browser for real-time video playback.
Common Errors with useRef
1. Reading ref.current too early
Accessing ref.current before the component mounts returns the initial value (usually null).
function BadRef() {
const inputRef = useRef(null);
// ❌ inputRef.current is null here — DOM not ready yet
inputRef.current.focus();
return <input ref={inputRef} />;
}
Fix: Access refs inside useEffect or event handlers — after the DOM is mounted.
2. Using ref instead of state for UI data
Refs don't trigger re-renders. If you store UI data in a ref, the screen won't update.
function BadCounter() {
const countRef = useRef(0);
function increment() {
countRef.current += 1; // Updates ref but NO re-render
}
return <p>{countRef.current}</p>; // Never updates on screen
}
Fix: Use useState for anything displayed on screen.
3. Forgetting refs persist across renders
Unlike local variables (which reset every render), refs keep their value. This is a feature, but can be surprising.
4. Using refs for prop-like values
Refs should not replace props for passing data to child components. Use props for that.
5. Mutating refs inside render without thought
Mutating a ref during rendering is fine for things like render counts, but avoid complex side effects in the render phase.
6. Not cleaning up ref values on unmount
If a ref holds a timer, WebSocket, or subscription, clear it in a useEffect cleanup.
7. Overusing useRef when useState would be simpler
Ask yourself: "Does this change need to be reflected on screen?" If yes, use state.
Practice Questions
What does
useRefreturn? A mutable object with a.currentproperty initialized to the passed argument.Does updating a ref cause a re-render? No. Changing
ref.currentdoes not trigger a re-render. This is the key difference fromuseState.When can you safely access
ref.currentfor a DOM element? After the component has mounted — insideuseEffector an event handler.Why not use
document.querySelectorinstead of useRef? useRef is tied to the component lifecycle, works with React's virtual DOM, and avoids fragile CSS selectors.Can you pass a ref to a custom component? Yes, but the custom component must use
forwardRefto accept the ref and attach it to a DOM element.
Challenge
Build a measuring tool component that:
- Renders a
<div>with dynamic text content entered by the user - Uses a ref to measure the rendered width/height of the div
- Displays the measurements in real-time
- Updates measurements when the text changes
// Starter code — complete the implementation
function MeasurementTool() {
const [text, setText] = useState("");
const divRef = useRef(null);
const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
// Your code here
// 1. Use useEffect to measure the div after render
// 2. Update dimensions state with ref.current measurements
// 3. Handle the case where divRef.current is null
return (
<div style={{ padding: "20px" }}>
<input value={text} onChange={e => setText(e.target.value)} />
<div ref={divRef} style={{ display: "inline-block", border: "1px solid" }}>
{text || "Type to see me"}
</div>
<p>Width: {dimensions.width}px, Height: {dimensions.height}px</p>
</div>
);
}
Real-World Task: Form Auto-Save
Build a form that auto-saves draft content to localStorage every 5 seconds using a ref for the interval:
function AutoSaveForm() {
const [formData, setFormData] = useState({ title: "", content: "" });
const saveIntervalRef = useRef(null);
useEffect(() => {
saveIntervalRef.current = setInterval(() => {
localStorage.setItem("draft", JSON.stringify(formData));
console.log("Draft saved at", new Date().toLocaleTimeString());
}, 5000);
return () => clearInterval(saveIntervalRef.current);
}, [formData]);
return (
<div style={{ padding: "20px" }}>
<input
value={formData.title}
onChange={e => setFormData(f => ({ ...f, title: e.target.value }))}
placeholder="Title"
/>
<textarea
value={formData.content}
onChange={e => setFormData(f => ({ ...f, content: e.target.value }))}
placeholder="Content"
/>
</div>
);
}
Learning Path
graph LR
A[React Basics] --> B[useState Hook]
B --> C[useEffect Hook]
C --> D[Context API]
D --> E[useReducer Hook]
E --> F[Custom Hooks]
F --> G[useRef Hook]
G --> H[React Router]
G --> I[Performance Optimization]
style G fill:#ff6b6b,stroke:#333,stroke-width:2px,color:#fff
Related Tutorials
- React Tutorial for Beginners — start here if you're new
- React Hooks Complete Guide — overview of all hooks
- Learn about the useState hook for state management
- Master the useEffect hook for data fetching
- Explore the useReducer hook for advanced logic
Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro. Last updated: June 2026.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro