Mastering useRef in React: The Hidden Power Behind Mutable References

Mastering useRef in React: The Hidden Power Behind Mutable References
Photo by Photo Boards / Unsplash

When you first dive into React, you often hear about useState and useEffect. But there’s another hook that quietly does a lot of heavy lifting behind the scenes — useRef.
Although often overlooked, useRef is one of the most powerful tools for handling persistent values, DOM references, and performance optimization.

Let’s explore how and when to use it effectively — with practical examples and deeper insights that developers often miss.


🔍 What Is useRef?

useRef is a React Hook that returns a mutable object that persists for the entire lifetime of a component.
It looks like this:

const ref = useRef(initialValue);

The returned object always has a .current property that you can freely mutate without triggering a re-render.


🧱 1. Accessing and Controlling DOM Elements

The most common and intuitive use of useRef is directly referencing DOM elements — something you can’t easily do with useState.

Example:

import { useRef } from "react";

function FocusInput() {
  const inputRef = useRef(null);

  const handleFocus = () => {
    inputRef.current.focus();
  };

  return (
    <div>
      <input ref={inputRef} type="text" placeholder="Click the button to focus" />
      <button onClick={handleFocus}>Focus Input</button>
    </div>
  );
}

Here, inputRef.current gives direct access to the <input> element in the DOM, allowing you to call .focus().

When to Use:

  • You need imperative control (focus, scroll, play, pause, etc.).
  • You integrate with third-party libraries that need direct DOM access (like D3.js, Leaflet, or video players).
  • You want to measure DOM nodes (height, width, position).

⚙️ 2. Storing Mutable Data Without Re-renders

Unlike useState, updating useRef.current does not trigger a re-render.
This makes it perfect for storing data that changes over time but doesn’t affect what’s rendered.

Example:

import { useEffect, useRef } from "react";

function Timer() {
  const countRef = useRef(0);

  useEffect(() => {
    const interval = setInterval(() => {
      countRef.current += 1;
      console.log("Count:", countRef.current);
    }, 1000);
    return () => clearInterval(interval);
  }, []);

  return <p>Check the console for count updates.</p>;
}

Even though countRef.current keeps changing, the component does not re-render, keeping it performant.

When to Use:

  • To store mutable state that should not trigger re-renders.
  • To keep previous values for comparison (like previous props or scroll positions).
  • To store timeouts, intervals, or event listeners.

🧠 3. Storing Previous Values

You can also use useRef to remember previous props or state across renders.

Example:

import { useEffect, useRef } from "react";

function PreviousValue({ value }) {
  const prevValueRef = useRef();

  useEffect(() => {
    prevValueRef.current = value;
  });

  return (
    <p>
      Current: {value}, Previous: {prevValueRef.current}
    </p>
  );
}

This technique is excellent for detecting changes or performing custom diffing logic between renders.


🚀 4. Avoiding Re-creation of Functions or Objects

Each time a component renders, functions and objects are recreated.
If you want to store something between renders without re-instantiation, useRef helps.

Example:

const instanceRef = useRef(createExpensiveObject());

By storing it in a ref, you ensure the object persists throughout the component’s lifetime — improving performance and avoiding unnecessary recalculations.


⚡ 5. Managing Animation Frames or Event Handlers

In cases like animations, drag events, or audio visualizers, useRef is invaluable for storing data that updates continuously without state overhead.

Example (animation frame):

function AnimatedBox() {
  const requestRef = useRef();

  const animate = () => {
    // animation logic here
    requestRef.current = requestAnimationFrame(animate);
  };

  useEffect(() => {
    requestRef.current = requestAnimationFrame(animate);
    return () => cancelAnimationFrame(requestRef.current);
  }, []);
}

Without useRef, you’d struggle to maintain references across frames efficiently.


🧩 Key Differences: useRef vs useState

Feature useRef useState
Causes re-render on update ❌ No ✅ Yes
Stores mutable value ✅ Yes ⚠️ Not directly
Ideal for DOM manipulation ✅ Yes ❌ No
Ideal for rendering data ❌ No ✅ Yes
Value persists between renders ✅ Yes ✅ Yes

In short:
Use useState for render-driven data, and useRef for logic-driven or DOM-driven data.


💡 Additional Considerations

  1. Avoid Overusing useRef
    It’s tempting to use useRef for everything mutable, but overusing it can lead to hard-to-debug side effects. Keep render logic declarative when possible.
  2. Don’t Rely on useRef for Triggering UI Changes
    Remember, updating ref.current doesn’t cause React to re-render.
    If you need the UI to update, stick with useState.
  3. useRef and Strict Mode
    React’s Strict Mode may call your component function twice during development.
    Avoid initializing refs with non-idempotent logic directly inside useRef(initialValue).
  4. useRef in Concurrent Rendering
    Because refs are mutable and persist across renders, they’re safe in concurrent React, as they don’t interfere with the reconciliation process.

🧭 Finally

The useRef hook may look simple, but it’s one of React’s most versatile tools.
Whether you’re accessing DOM elements, keeping state without re-renders, or optimizing performance, mastering useRef separates good React developers from great ones.

Think of it as your React memory box — a place to store anything that should survive re-renders but not control rendering itself.

Support Us

Share to Friends