React Refs & useRef — The 'Secret Backdoor' to the DOM 🚪
Source: Dev.to
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.
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.
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} />;
}
What happens
useRef(null)creates an object:{ current: null }.- The object is passed to the
refprop on<input>. - React fills
inputRef.currentwith the actual DOM node of that input.
Now inputRef.current is the real <input> element on the page.
A Real‑World Example: Auto‑Scroll to New Content
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 && (
<section ref={recipeSectionRef}>
<h2>{recipe.title}</h2>
<p>{recipe.instructions}</p>
</section>
)}
</div>
);
}
What’s happening step by step
- User clicks Get a Recipe.
- The API returns data → state updates → React re‑renders.
- The
<section>with our ref now exists in the DOM. useEffectruns, 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?”
You could do this:
...
document.getElementById("recipe-section").scrollIntoView();
It works… until it doesn’t. React encourages reusable components. Rendering the same component twice would create duplicate IDs, which is invalid HTML and a source of bugs.
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 (myRef.current = …) |
| Common use | UI data | DOM access, timers, previous values |
| Shape | Whatever you set | { current: value } |
Three Quick Rules to Remember
-
Refs are just boxes.
useRef(initialValue)returns{ current: initialValue }– a box with one shelf calledcurrent. -
Mutate freely.
Unlike state, you can assignmyRef.current = "whatever"without causing a re‑render. -
The
refprop is magic — but only on native elements.
<input ref={myRef}>makes React fillmyRef.currentwith that DOM node.
<MyComponent ref={myRef}>merely passes a regular prop namedrefunless you useforwardRef.
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. - Ideal for scrolling, focusing inputs, measuring elements, or storing values between renders without triggering updates.
Refs feel odd at first, but once you “get” them, they become second nature. Use them whenever you need a direct handle on the DOM or a stable mutable value across renders.