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

Published: (February 15, 2026 at 01:09 PM EST)
3 min read
Source: Dev.to

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

  1. useRef(null) creates an object: { current: null }.
  2. The object is passed to the ref prop on <input>.
  3. React fills inputRef.current with 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

  1. User clicks Get a Recipe.
  2. The API returns data → state updates → React re‑renders.
  3. The <section> with our ref now exists in the DOM.
  4. useEffect runs, 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?”

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

StateRef
Triggers re‑render?YesNo
How to updateSetter functionDirect mutation (myRef.current = …)
Common useUI dataDOM access, timers, previous values
ShapeWhatever you set{ current: value }

Three Quick Rules to Remember

  1. Refs are just boxes.
    useRef(initialValue) returns { current: initialValue } – a box with one shelf called current.

  2. Mutate freely.
    Unlike state, you can assign myRef.current = "whatever" without causing a re‑render.

  3. The ref prop is magic — but only on native elements.
    <input ref={myRef}> makes React fill myRef.current with that DOM node.
    <MyComponent ref={myRef}> merely passes a regular prop named ref unless you use forwardRef.

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.
  • 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.

0 views
Back to Blog

Related posts

Read more »