Ever needed to talk directly to a DOM element in React, but felt like React was standing in your way?
That's exactly what useRef is for. Think of it as a secret backdoor that lets you reach into the actual DOM — without breaking any of React's rules.
Let's break it down so simply that you'll never forget it.
State vs. Ref — The Two-Sentence Version
- State → Changes trigger a re-render. You update it with a setter function.
- Ref → Changes are silent. You mutate it directly, and React doesn't even blink.
That's the core difference. Refs are like sticky notes you keep for yourself. React doesn't care what you write on them.
Creating a Ref
import { useRef } from "react";
function MyComponent() {
const inputRef = useRef(null);
return <input ref={inputRef} />;
}
Three things just happened:
-
useRef(null)created an object:{ current: null } - We passed that object to the
refprop on the<input> - React filled in
inputRef.currentwith the actual DOM node of that input
That's it. inputRef.current is now the real, living, breathing <input> element on the page.
A Real-World Example: Auto-Scroll to New Content
Imagine you have an app where a user clicks a button, waits for data to load, and the new content appears below the fold. The user has no idea anything happened. Bad UX.
Here's how refs fix that:
import { useRef, useEffect, useState } from "react";
function RecipeApp() {
const [recipe, setRecipe] = useState(null);
const recipeSectionRef = useRef(null);
async function fetchRecipe() {
const response = await getRecipeFromAI(); // pretend API call
setRecipe(response);
}
useEffect(() => {
if (recipe && recipeSectionRef.current) {
recipeSectionRef.current.scrollIntoView({ behavior: "smooth" });
}
}, [recipe]);
return (
<div>
<button onClick={fetchRecipe}>Get a Recipe</button>
{recipe && (
<div ref={recipeSectionRef}>
<h2>{recipe.title}</h2>
<p>{recipe.instructions}</p>
</div>
)}
</div>
);
}
What's happening step by step:
- User clicks "Get a Recipe"
- The API returns data → state updates → React re-renders
- The
<div>with our ref now exists in the DOM -
useEffectfires, sees the recipe is loaded, and callsscrollIntoView() - The browser smoothly scrolls down to the recipe section
No document.getElementById. No query selectors. Just a clean ref.
"But Why Not Just Use an ID?"
Great question. You could do this:
<div id="recipe-section">...</div>
// somewhere else:
document.getElementById("recipe-section").scrollIntoView();
It works... until it doesn't. Here's the problem:
React is built around reusable components. If you render the same component twice, you get two elements with the same ID on the page. That's invalid HTML and a bug waiting to happen.
Refs avoid this entirely because they're scoped to each component instance. Two instances, two separate refs, zero conflicts.
The Mental Model Cheat Sheet
| State | Ref | |
|---|---|---|
| Triggers re-render? | Yes | No |
| How to update | Setter function | Direct mutation |
| Common use | UI data | DOM access, timers, previous values |
| Shape | Whatever you set | { current: value } |
Three Quick Rules to Remember
Rule 1: Refs are just boxes.
useRef(initialValue) gives you { current: initialValue }. That's the whole data structure. A box with one shelf called current.
Rule 2: Mutate freely.
Unlike state, you can do myRef.current = "whatever" and React won't complain or re-render.
Rule 3: The ref prop is magic — but only on native elements.
When you write <div ref={myRef}>, React automatically fills myRef.current with that DOM node. But if you write <MyComponent ref={myRef}>, you're just passing a regular prop called "ref" (unless you use forwardRef, which is a story for another day).
TL;DR
-
useRefcreates a persistent mutable container:{ current: value } - Changing
.currentdoes not cause a re-render - Attach it to a DOM element via the
refprop to get direct access to that node - Perfect for things like scrolling, focusing inputs, measuring elements, or storing values between renders without triggering updates
Refs are one of those tools that feel weird at first and then become second nature. Once you "get" them, you'll reach for them all the time.
Top comments (2)