DEV Community

Mohamed Idris
Mohamed Idris

Posted on

React Refs & useRef — The "Secret Backdoor" to the DOM 🚪

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} />;
}
Enter fullscreen mode Exit fullscreen mode

Three things just happened:

  1. useRef(null) created an object: { current: null }
  2. We passed that object to the ref prop on the <input>
  3. React filled in inputRef.current with 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>
  );
}
Enter fullscreen mode Exit fullscreen mode

What's happening step by step:

  1. User clicks "Get a Recipe"
  2. The API returns data → state updates → React re-renders
  3. The <div> with our ref now exists in the DOM
  4. useEffect fires, sees the recipe is loaded, and calls scrollIntoView()
  5. 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();
Enter fullscreen mode Exit fullscreen mode

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

  • useRef creates a persistent mutable container: { current: value }
  • Changing .current does not cause a re-render
  • Attach it to a DOM element via the ref prop 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)

Collapse
 
edriso profile image
Mohamed Idris

For anyone curious what "React's rules" means here — React really wants you to treat state as immutable. Don't push to an array, don't reassign an object property. Always use the setter function so React knows something changed and can re-render. Refs are the one place where React says "go ahead, mutate directly" — because the whole point is that React doesn't need to know about the change. That's what makes them special.

Collapse
 
edriso profile image
Mohamed Idris