React useRef Explained with Real-World Examples

by Didin J. on Nov 23, 2025 React useRef Explained with Real-World Examples

Learn how to use React’s useRef hook with clear explanations and real-world examples. Master DOM access, timers, previous values, and performance tips.

Managing state and interacting with the DOM efficiently is a core skill for every React developer. While most developers are familiar with useState and useEffect, another powerful React Hook often goes underused: useRef.

useRef allows you to persist values across renders without causing re-renders, making it ideal for storing mutable values, accessing DOM elements directly, tracking previous values, controlling timers, and more. Whether you're building forms, handling animations, or improving performance, understanding useRef can unlock cleaner and more predictable React code.

In this tutorial, we’ll break down how useRef works, when you should use it, and provide practical, real-world examples you can immediately apply to your projects. By the end, you'll have a solid grasp of how to use useRef effectively—especially in scenarios where other hooks may fall short.

Let’s dive in!


What is useRef?

useRef is a built-in React Hook that lets you store a mutable value that persists across component renders. Unlike useState, updating a useRef value does not trigger a re-render, making it perfect for cases where you need to keep track of something without affecting the UI.

You can think of a ref as a simple JavaScript object with a single property:

const ref = {
  current: value
};

In React, you create it using:

const myRef = useRef(initialValue);

Key Characteristics of useRef

  • Mutable container: The value is stored in myRef.current, and you can change it anytime.

  • Does not cause re-renders: Updating the ref won't update the component UI.

  • Persists across renders: Useful for storing data that needs to survive component updates.

  • Can reference DOM elements: The most common use case—accessing or controlling DOM nodes.

When to Use useRef (High-Level Use Cases)

  • Accessing or manipulating DOM elements

  • Storing values that should persist but should not cause UI updates

  • Tracking previous values

  • Storing timeouts, intervals, or event listeners

  • Avoiding re-creating expensive objects on each render

Example: Creating a Basic Ref

import { useRef } from "react";

export default function BasicRefExample() {
  const countRef = useRef(0);

  const increment = () => {
    countRef.current++;
    console.log("Current count:", countRef.current);
  };

  return (
    <button onClick={increment}>
      Click to increase (check console)
    </button>
  );
}

Here, each click updates the ref, but the component does not re-render—only the console output changes.


Understanding Refs and the DOM

One of the most common and practical uses of useRef is to access and interact with DOM elements directly. While React encourages declarative UI updates, there are still times when direct DOM manipulation is necessary—such as handling focus, playing/pausing videos, or measuring element size.

useRef provides a clean and React-friendly way to do this.

How Refs Work with DOM Elements

When you attach a ref to a DOM element, React assigns the actual DOM node to the ref’s current property:

const inputRef = useRef(null);

<input ref={inputRef} />

After the component renders, inputRef.current will hold the real underlying DOM element, allowing you to perform actions on it.

Example: Focusing an Input Field

Here’s a simple example to automatically focus an input when a component loads:

import { useEffect, useRef } from "react";

export default function AutoFocusInput() {
  const inputRef = useRef(null);

  useEffect(() => {
    inputRef.current.focus();
  }, []);

  return (
    <input
      ref={inputRef}
      type="text"
      placeholder="This input will auto-focus"
    />
  );
}

How it works:

  • After the component mounts, the useEffect runs.

  • inputRef.current refers to the input DOM element.

  • Calling .focus() gives the input field focus.

Example: Scroll to an Element

Refs also make it easy to scroll directly to specific areas of the page:

import { useRef } from "react";

export default function ScrollExample() {
  const sectionRef = useRef(null);

  const scrollToSection = () => {
    sectionRef.current.scrollIntoView({ behavior: "smooth" });
  };

  return (
    <>
      <button onClick={scrollToSection}>Scroll to Section</button>

      <div style={{ height: "1000px" }}></div>

      <div ref={sectionRef}>
        <h2>Target Section</h2>
      </div>
    </>
  );
}

This approach is especially useful for:

  • “Scroll to top/bottom” buttons

  • Navigating to sections in landing pages

  • Chat UIs that auto-scroll to the latest message

Avoid Overusing DOM Manipulation

While refs make it easy to work with the DOM, keep these best practices in mind:

  • Prefer React’s declarative approach first.

  • Use refs only when you need direct DOM access.

  • Avoid complex DOM manipulations—React may overwrite or conflict with them.


Tracking Mutable Values Without Re-rendering

One of the most powerful advantages of useRef is that it lets you store mutable values that persist between renders without causing a re-render. This makes it perfect for tracking data that changes often but doesn’t need to immediately affect the UI.

While useState triggers a component update every time its value changes, useRef simply updates the .current property—quietly and efficiently.

When to Use useRef Instead of useState

Use useRef when:

  • You want to update a value frequently but don’t want UI updates.

  • You need a value that persists across renders.

  • You’re tracking something for internal logic, not for display.

Examples:

  • Scroll or mouse position

  • Timeout or interval IDs

  • Count of function calls

  • Storing previous state values

  • Storing a flag (e.g., "is first render")

Example: Counting Renders

Every time a component renders, we can increment a ref to track how many times it rendered—without triggering another render.

import { useEffect, useRef, useState } from "react";

export default function RenderCounter() {
  const [value, setValue] = useState("");
  const renderCount = useRef(1);

  useEffect(() => {
    renderCount.current++;
  });

  return (
    <div>
      <input
        value={value}
        onChange={(e) => setValue(e.target.value)}
        placeholder="Type something"
      />

      <p>Renders: {renderCount.current}</p>
    </div>
  );
}

Explanation:

  • The component re-renders every time the input changes.

  • renderCount.current increments but does not cause another render.

  • This is ideal for debugging and optimization.

Example: Store Timeout or Interval IDs

Storing timers in useRef prevents unnecessary re-renders and gives you stable references.

import { useRef } from "react";

export default function TimerExample() {
  const timerRef = useRef(null);

  const startTimer = () => {
    timerRef.current = setTimeout(() => {
      console.log("Timer finished!");
    }, 2000);
  };

  const clearTimer = () => {
    clearTimeout(timerRef.current);
  };

  return (
    <div>
      <button onClick={startTimer}>Start Timer</button>
      <button onClick={clearTimer}>Clear Timer</button>
    </div>
  );
}

This technique is widely used for:

  • Debouncing

  • Throttling

  • Auto-save timers

  • Delayed UI events

Key Takeaway

useRef is perfect when:

  • You need a value that must persist,

  • It changes often,

  • And updating it should not cause a re-render.

It helps keep your logic clean and your UI efficient.


Storing Previous State or Props

Another powerful use case for useRef is storing the previous value of state or props. Since refs persist across renders without causing re-renders, they are ideal for tracking "what the value was before."

React does not provide a built-in “previous value” hook, but implementing one with useRef is simple and highly reusable.

Why Track Previous Values?

Tracking previous values is useful for features like:

  • Detecting value changes (e.g., form validation, animations)

  • Comparing current vs. previous props in a custom hook

  • Triggering actions only when a value changes in a specific way

  • Debugging state transitions

Example: Tracking the Previous State Value

import { useEffect, useRef, useState } from "react";

export default function PreviousStateExample() {
  const [count, setCount] = useState(0);
  const prevCountRef = useRef();

  useEffect(() => {
    prevCountRef.current = count;
  }, [count]);

  return (
    <div>
      <p>Current count: {count}</p>
      <p>Previous count: {prevCountRef.current}</p>

      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
}

How it works:

  1. On every render where count changes, the effect runs.

  2. The previous value is stored in prevCountRef.current.

  3. The component can display or use the previous value without re-render loops.

Reusable Custom Hook: usePrevious

You can encapsulate this logic into a reusable hook:

import { useEffect, useRef } from "react";

export function usePrevious(value) {
  const ref = useRef();

  useEffect(() => {
    ref.current = value;
  }, [value]);

  return ref.current;
}

Usage:

const prevName = usePrevious(name);

This pattern is extremely common across React codebases.

Example: Detecting Prop Changes

Here’s how to track changes in props:

import { usePrevious } from "./usePrevious";

export default function UserProfile({ name }) {
  const prevName = usePrevious(name);

  return (
    <div>
      <p>Current name: {name}</p>
      <p>Previous name: {prevName}</p>
    </div>
  );
}

This technique helps with:

  • Syncing external values

  • Animating transitions

  • Conditional logic based on previous props

Key Takeaway

useRef lets you easily store previous values without creating unnecessary renders.
This pattern keeps your components clean and your logic predictable.


useRef vs useState — When to Use Which?

React provides both useState and useRef to store values, but they behave differently and serve different purposes. Choosing the right one can significantly impact your component’s performance and clarity.

Let’s break down the differences and when each hook should be used.

Key Differences at a Glance

Feature useState useRef
Triggers re-render on update ✅ Yes ❌ No
Stores mutable values ⚠️ Avoid updating directly ✅ Yes
Persists across renders ✅ Yes ✅ Yes
Good for DOM references ❌ No ✅ Yes
Best used for UI state ✅ Yes ❌ No
Good for values unrelated to UI ❌ No ✅ Yes

When to Use useState

Use useState when:

  • The value should affect the UI.

  • Changing the value should trigger a re-render.

  • You need to display the value somewhere in your JSX.

  • You want React to manage and track changes over time.

Examples:

  • Form inputs

  • Toggles, dropdowns, UI visibility

  • Counters

  • Data fetched from an API

Rule of thumb:
If the value appears in your JSX → useState.

When to Use useRef

Use useRef when:

  • The value does not need to cause a UI update.

  • You need a mutable, persistent container.

  • You need direct access to a DOM element.

  • You want to store values between renders without triggering re-renders.

Examples:

  • Timer IDs

  • Tracking previous values

  • Storing scroll positions

  • Handling focus, play, pause events

  • Storing a flag (e.g., "hasMounted")

Rule of thumb:
If the value should persist but not trigger re-renders → useRef.

Example: Wrong vs. Right Usage

❌ Incorrect: Using useState for a value that doesn’t affect UI

const [clicks, setClicks] = useState(0);

const handleClick = () => {
  setClicks(clicks + 1); // Causes unnecessary re-render
};

✅ Correct: Using useRef instead

const clicks = useRef(0);

const handleClick = () => {
  clicks.current++; // No re-render needed
};

Hybrid Use: useState + useRef Together

In many real-world components, you’ll use both:

Example:

  • useState → manages UI

  • useRef → stores internal logic or DOM references

const [isOpen, setIsOpen] = useState(false);
const dropdownRef = useRef(null);

This combination is common in modals, dropdowns, tools with animations, etc.

Key Takeaway

  • If your value influences what the user sees → useState

  • If your value is internal logic or DOM interaction → useRef

Choosing correctly improves performance, reduces unnecessary renders, and keeps your components clean.


Real-World Example 1 — Auto-Focus and Form Handling

One of the most common real-world uses of useRef is handling form fields—especially when you need to auto-focus inputs or manually control form behavior.

In many cases, you want to focus an input when:

  • The page loads

  • A modal opens

  • A form validation error occurs

  • A user completes another field

Using useRef, you can interact directly with the input DOM element while keeping your component clean and declarative.

Example: Auto-Focus on Form Load

import { useEffect, useRef } from "react";

export default function AutoFocusForm() {
  const nameRef = useRef(null);

  useEffect(() => {
    nameRef.current.focus();
  }, []);

  return (
    <form>
      <input ref={nameRef} type="text" placeholder="Enter your name" />
    </form>
  );
}

How it works:

  • The input ref is attached to the DOM node.

  • When the component mounts, useEffect focuses the input

  • No re-render is triggered.

This is ideal for login forms, search boxes, or productivity tools.

Example: Focus on Validation Error

A more realistic scenario: directing focus to the field with an error.

import { useRef, useState } from "react";

export default function ValidationForm() {
  const nameRef = useRef(null);
  const [name, setName] = useState("");
  const [error, setError] = useState("");

  const handleSubmit = (e) => {
    e.preventDefault();

    if (name.trim() === "") {
      setError("Name is required");
      nameRef.current.focus();
      return;
    }

    setError("");
    alert(`Submitted: ${name}`);
  };

  return (
    <form onSubmit={handleSubmit}>
      <div>
        <input
          ref={nameRef}
          value={name}
          onChange={(e) => setName(e.target.value)}
          placeholder="Name"
        />
        {error && <p style={{ color: "red" }}>{error}</p>}
      </div>

      <button type="submit">Submit</button>
    </form>
  );
}

What this example demonstrates:

  • Ref gives direct control over the input element.

  • When validation fails, focus immediately returns to the problem field.

  • Enhances user experience and accessibility.

Why useRef Is Perfect for This

  • Input fields are DOM elements → refs provide access.

  • Focusing fields shouldn’t cause re-renders.

  • Logic stays simple and predictable.

  • No need to use the state just to control focus.

Best Practices for Using Refs in Forms

  • Use refs for imperative actions (focus, scroll, select).

  • Use state for form values.

  • Don’t replace controlled components with refs unless necessary.

  • Avoid overusing refs—keep form logic declarative when possible.


Real-World Example 2 — Managing Timers and Intervals

Timers (such as setTimeout and setInterval) are common in React apps, especially for features like:

  • Auto-save functionality

  • Debouncing user actions

  • Countdown timers

  • Delayed UI effects

  • Polling APIs

However, storing timer IDs in useState is inefficient because it causes unnecessary re-renders.
This is exactly where useRef shines.

useRef lets you store a timer ID that persists across renders without triggering updates.

Example: Start & Stop a Timer

import { useRef, useState } from "react";

export default function TimerController() {
  const timerRef = useRef(null);
  const [count, setCount] = useState(0);

  const startTimer = () => {
    if (timerRef.current) return; // Prevent multiple intervals

    timerRef.current = setInterval(() => {
      setCount((prev) => prev + 1);
    }, 1000);
  };

  const stopTimer = () => {
    clearInterval(timerRef.current);
    timerRef.current = null;
  };

  return (
    <div>
      <h3>Count: {count}</h3>

      <button onClick={startTimer}>Start</button>
      <button onClick={stopTimer}>Stop</button>
    </div>
  );
}

Why this approach works well:

  • Timer ID is stored in a ref, not state → no UI re-renders.

  • Interval is safely controlled without duplicating intervals.

  • Changing the count uses useState since it affects UI.

Example: Debounce Input (Real-World Search Box)

Debouncing prevents a function from running too frequently — perfect for search fields that hit an API.

import { useRef, useState } from "react";

export default function DebouncedSearch() {
  const [query, setQuery] = useState("");
  const timeoutRef = useRef(null);

  const handleChange = (e) => {
    const value = e.target.value;
    setQuery(value);

    clearTimeout(timeoutRef.current);

    timeoutRef.current = setTimeout(() => {
      console.log("Searching for:", value);
      // Call API here
    }, 500);
  };

  return (
    <input
      type="text"
      value={query}
      onChange={handleChange}
      placeholder="Search..."
    />
  );
}

What this example demonstrates:

  • Debounce logic is stored inside a ref.

  • Input updates instantly through the state.

  • API simulation runs only after the user stops typing.

This pattern is widely used in real applications.

Best Practices When Using Refs for Timers

  • Always clear timers in a useEffect cleanup if needed.

  • Guard against duplicate intervals.

  • Keep timer logic separate from UI state.

  • Avoid keeping timer IDs in state—there’s no UI benefit.


Real-World Example 3 — Tracking Previous Values

Tracking previous values is extremely useful in React apps, especially for comparing changes over time. Since React does not provide this feature out of the box, useRef becomes the perfect tool.

You can track the previous value of:

  • Form inputs

  • Props passed from parent components

  • API responses

  • UI states like toggle values

  • Animation states

This enables features like:

  • Detecting changes

  • Running logic only when certain conditions occur

  • Debugging state transitions

Example: Track Previous Prop Value

Imagine you receive a userId prop and want to detect when it changes to refetch data or log something.

import { useEffect, useRef } from "react";

export default function UserInfo({ userId }) {
  const prevUserId = useRef(null);

  useEffect(() => {
    if (prevUserId.current !== null && prevUserId.current !== userId) {
      console.log("User changed:", prevUserId.current, "→", userId);
    }

    prevUserId.current = userId;
  }, [userId]);

  return <p>Current User ID: {userId}</p>;
}

What this example demonstrates:

  • prevUserId.current always holds the previous userId.

  • The effect only logs when the value actually changes.

  • No unnecessary re-renders occur.

Example: Animations Triggered by Value Change

Tracking previous values is also helpful in UI animations:

import { useEffect, useRef, useState } from "react";

export default function BalanceTracker() {
  const [balance, setBalance] = useState(100);
  const prevBalance = useRef(balance);

  useEffect(() => {
    if (prevBalance.current < balance) {
      console.log("Balance increased");
      // Trigger animation here
    } else if (prevBalance.current > balance) {
      console.log("Balance decreased");
      // Trigger animation here
    }

    prevBalance.current = balance;
  }, [balance]);

  return (
    <div>
      <p>Balance: ${balance}</p>
      <button onClick={() => setBalance(balance + 10)}>Add 10</button>
      <button onClick={() => setBalance(balance - 10)}>Remove 10</button>
    </div>
  );
}

This technique mirrors real product behaviors like:

  • Wallet apps

  • E-commerce price changes

  • Gaming stats

  • Animation-based data dashboards

Reusable Hook (Best Practice): usePrevious

As shown earlier, this helper hook simplifies tracking previous values:

import { useEffect, useRef } from "react";

export function usePrevious(value) {
  const ref = useRef();

  useEffect(() => {
    ref.current = value;
  }, [value]);

  return ref.current;
}

Then use it like this:

const previousValue = usePrevious(currentValue);

This keeps your components clean and reusable.

Key Takeaway

useRef makes it easy to track previous values without causing re-renders.
This is a foundational pattern used across modern React codebases, especially for comparisons, debugging, and triggering side effects.


Real-World Example 4 — Persisting Values Between Renders Without Re-rendering

Sometimes, you need to store a value that persists across renders but doesn’t belong in the UI and shouldn’t trigger any re-render. This is exactly what useRef was designed for.

These persistent values are often used for:

  • Caching expensive computations

  • Storing configuration objects

  • Keeping API response snapshots

  • Tracking event listeners or subscription status

  • Keeping flags (e.g., “has initialized”, “is first render”)

Because the value inside a ref does not cause re-renders when updated, it’s ideal for internal logic and performance optimization.

Example: Persisting Expensive Computation Results

Suppose you have a CPU-heavy calculation that doesn’t need to re-run every render.

import { useRef, useState } from "react";

function expensiveCalculation(num) {
  console.log("Running expensive calculation...");
  return num * 1000;
}

export default function ExpensiveComputeExample() {
  const [input, setInput] = useState("");
  const cacheRef = useRef({});

  const handleCalculate = () => {
    if (!cacheRef.current[input]) {
      cacheRef.current[input] = expensiveCalculation(input);
    }
  };

  return (
    <div>
      <input
        type="number"
        value={input}
        onChange={(e) => setInput(e.target.value)}
        placeholder="Enter a number"
      />

      <button onClick={handleCalculate}>Calculate</button>

      <p>Cached Result: {cacheRef.current[input]}</p>
    </div>
  );
}

Why this is useful:

  • Expensive logic is skipped if the result is already cached.

  • Cache persists across renders.

  • UI updates only when necessary (via state).

Example: Track Whether a Component Has Mounted

This is a common need for preventing certain logic from running on the first render.

import { useEffect, useRef, useState } from "react";

export default function FirstRenderTracker() {
  const [value, setValue] = useState("");
  const isFirstRender = useRef(true);

  useEffect(() => {
    if (isFirstRender.current) {
      isFirstRender.current = false;
      return;
    }

    console.log("Value changed:", value);
  }, [value]);

  return (
    <input
      value={value}
      onChange={(e) => setValue(e.target.value)}
      placeholder="Type something"
    />
  );
}

Benefits:

  • Cleaner than useEffect dependency hacks.

  • No re-renders triggered by updating the ref.

  • Logic runs only when the value changes after the first render.

Example: Persistent Mutable Object for Event Handlers

Useful for websockets, subscriptions, or tracking state without closure issues.

import { useEffect, useRef } from "react";

export default function EventTracker() {
  const eventCountRef = useRef(0);

  const handleClick = () => {
    eventCountRef.current++;
    console.log("Clicks so far:", eventCountRef.current);
  };

  useEffect(() => {
    window.addEventListener("click", handleClick);
    return () => window.removeEventListener("click", handleClick);
  }, []);

  return <p>Click anywhere and check console</p>;
}

Here:

  • The count persists across renders.

  • It updates without causing re-renders.

  • No stale closures — the ref always holds the latest value.

Key Takeaway

useRef gives you persistent storage that’s:

  • Mutable

  • Fast

  • Independent of UI updates

This makes it ideal for internal logic, caching, performance, and event-based state.


Real-World Example 5 — Accessing and Controlling DOM Elements

One of the most popular and practical uses of useRef is to interact directly with DOM elements. While React encourages a declarative approach, there are many situations where you need imperative control over an element—such as focusing, scrolling, selecting text, or controlling media elements like <video> and <audio>.

With useRef, React hands you the actual DOM node through the .current property, which you can then manipulate just like in vanilla JavaScript.

Example: Select Text Input On Click

Sometimes you want an input that selects all text when the user clicks on it—useful in coupon codes, promo codes, shortlinks, or copyable values.

import { useRef } from "react";

export default function SelectOnClick() {
  const inputRef = useRef(null);

  const handleClick = () => {
    inputRef.current.select();
  };

  return (
    <input
      ref={inputRef}
      defaultValue="CLICK ME TO SELECT ALL"
      onClick={handleClick}
    />
  );
}

What this shows:

  • inputRef.current is a direct DOM node.

  • .select() is a native browser method.

  • No state is needed; no re-renders occur.

Example: Controlling a Video Player

Refs are extremely useful for video or audio controls.

import { useRef } from "react";

export default function VideoPlayer() {
  const videoRef = useRef(null);

  const playVideo = () => videoRef.current.play();
  const pauseVideo = () => videoRef.current.pause();

  return (
    <div>
      <video
        ref={videoRef}
        width="300"
        src="https://www.w3schools.com/html/mov_bbb.mp4"
      />

      <div>
        <button onClick={playVideo}>Play</button>
        <button onClick={pauseVideo}>Pause</button>
      </div>
    </div>
  );
}

Great for:

  • Tutorials

  • Course platforms

  • Custom video controls

  • Media-heavy dashboards

Example: Scroll to Bottom (Chat UI)

Chat and messaging UIs often need to scroll to the newest message.

import { useEffect, useRef } from "react";

export default function ChatBox({ messages }) {
  const bottomRef = useRef(null);

  useEffect(() => {
    bottomRef.current.scrollIntoView({ behavior: "smooth" });
  }, [messages]);

  return (
    <div style={{ height: "200px", overflowY: "auto" }}>
      {messages.map((msg, i) => (
        <p key={i}>{msg}</p>
      ))}
      <div ref={bottomRef}></div>
    </div>
  );
}

How it works:

  • On every new message, React re-renders.

  • useEffect scrolls the .current element into view.

  • Smooth scrolling improves UX.

This pattern appears in:

  • Chat apps

  • Notification feeds

  • Activity logs

  • Infinite scroll UIs

Best Practices for DOM Control with useRef

  • Use refs only when declarative logic cannot achieve the same result.

  • Keep DOM manipulation minimal—React still owns the rendering.

  • Avoid modifying DOM structure with refs (React may override it).

  • Prefer refs for imperative operations like focus, play, scroll, and select.


Common Pitfalls and How to Avoid Them

While useRef is powerful and flexible, it’s also easy to misuse—especially when mixing it with state or complex DOM logic. Understanding the common pitfalls will help you write cleaner, more predictable, and bug-free React code.

1. Updating Refs and Expecting Re-renders

❌ Pitfall

Developers sometimes assume that updating a ref will cause the UI to update:

ref.current = newValue; // UI will NOT update

But refs do not trigger re-renders.

✅ Solution

Use state whenever the value needs to appear in the UI.

  • Use useState → for UI state

  • Use useRef → for internal logic

2. Storing JSX or Rendered Elements in Refs

Refs should hold values or DOM elements, not JSX.

// ❌ Incorrect
myRef.current = <div>Hello</div>;

This causes unpredictable behavior.

✅ Solution

Let React handle rendering; refs store values only.

3. Using Refs When State Is the Right Choice

Some developers overuse refs, especially beginners, avoiding state re-renders.

// ❌ Wrong: using ref for UI value
const usernameRef = useRef("");

Why it’s wrong:

  • Input will not update the UI unless manually forced.

  • Disrupts React’s controlled-component pattern.

✅ Solution

Use useState for form values and UI-related data.

4. Forgetting to Check If ref.current Exists

On the initial render, ref.current is often null (especially with DOM refs).

ref.current.doSomething(); // ❌ may crash

✅ Solution

Always check before accessing:

ref.current?.doSomething();

Or check inside useEffect after mount.

5. Creating Multiple Event Listeners Without Cleanup

When using refs with event listeners:

useEffect(() => {
  window.addEventListener("scroll", handleScroll);
});

This registers the listener on every render.

✅ Solution

Use an empty dependency array with cleanup:

useEffect(() => {
  window.addEventListener("scroll", handleScroll);
  return () => window.removeEventListener("scroll", handleScroll);
}, []);

Refs help store the latest function value, but you still need cleanup.

6. Misusing Refs for Derived State

Refs should not replace logic that can be handled by computed values or effects.

// ❌ Wrong: storing derived values
ref.current = expensiveCalculate(a, b);

Why is this wrong?

  • Hard to track

  • Easy to fall out of sync

  • Encourages imperative patterns

✅ Solution

Use useMemo or useEffect + state for derived data.

7. Mutating Objects in Refs Without Care

If ref.current holds an object, mutating it can create hard-to-track bugs.

ref.current.data.push(newItem); // ⚠️ hidden mutation

Solutions:

  • Treat refs like mutable boxes, but use them intentionally.

  • Document their purpose clearly.

  • Avoid storing complex state in refs unless absolutely needed.

8. Using Refs to Synchronize State Between Components

Refs are local to a component. They should not be used for cross-component communication.

❌ Bad:

Storing shared state in a ref and passing it around.

✅ Good:

Use React context, state management (Redux, Zustand, Jotai), or props.

Key Takeaway

Refs are powerful but should be used sparingly.
Use them when:

  • You need direct DOM access

  • You need mutable values that persist across renders

  • You have internal logic that should not affect UI

Avoid using refs for actual UI state or shared data.


Best Practices for Using useRef Effectively

useRef is a versatile hook, but using it correctly requires knowing when it helps—and when it introduces unnecessary complexity. Below are best practices to ensure your code remains clean, predictable, and aligned with React’s declarative philosophy.

1. Use Refs for Imperative Actions Only

Refs are excellent for interacting with the DOM when React’s declarative model isn’t enough.

Good uses:

  • Focus input fields

  • Play/pause a video

  • Scroll to a section

  • Select text

  • Trigger an animation manually

Tip: If you can express the behavior declaratively, avoid refs.

2. Use State for Anything That Affects the UI

A common mistake is using refs instead of state because they don’t trigger re-renders.

Rule of thumb:

  • If it appears on screen → useState

  • If it’s only used in logic → useRef

Example:

  • Form values → useState

  • Timer IDs → useRef

3. Avoid Storing Large or Complex Objects in Refs

While refs can store anything, large objects can cause:

  • Hidden mutations

  • Debugging difficulty

  • Unpredictable behavior

Keep ref values simple and intentional.

4. Be Careful When Mutating Ref Values

Mutating values inside refs won’t cause re-renders, which means bugs may go unnoticed.

ref.current.value++; // silently changes

Mitigation:

  • Document the purpose of a ref

  • Keep mutations localized

  • Avoid storing complex state in refs

5. Initialize DOM Refs with null

Always start DOM refs with null:

const inputRef = useRef(null);

This prevents unexpected behaviors and makes typechecking easier.

6. Combine useRef with useEffect for Mounted Elements

For DOM interactions, always use refs inside effects.

❌ Bad (may run before DOM exists):

inputRef.current.focus();

✅ Good:

useEffect(() => {
  inputRef.current?.focus();
}, []);

7. Use Refs to Avoid Stale Closures

When attaching event listeners or timers, refs help ensure your callback always has the latest value.

Use refs when:

  • The callback depends on a changing value

  • You’re adding global event listeners that persist

8. Avoid Passing Refs Deeply Into Child Components

Passing refs multiple layers down tightly couples components.

Prefer:

  • forwardRef if the child specifically exposes DOM access

  • Callback refs if you need fine-grained control

  • Props or state lifting for logic, not refs

9. Use useRef to Persist Values Between Renders

Refs are perfect for persistent flags like:

  • isMounted

  • isFirstRender

  • Cached results

  • Previous values

Just remember: these values won’t update the UI.

10. Keep Refs Simple to Reduce Mental Overhead

A ref should feel like a small, purpose-driven tool—not a substitute for state or context.

If your ref usage feels complex:

  • Re-evaluate your component design

  • Consider whether state or derived values are more appropriate

Key Takeaway

useRef is most effective when:

  • You need persistence without re-rendering

  • You’re working with the DOM directly

  • You’re handling internal logic or performance optimizations

Used wisely, it can greatly simplify your components while keeping them performant and predictable.

1. When the Value Should Trigger a UI Update

If the value affects what the user sees on the screen, it belongs in the state, not a ref.

❌ Don’t use:

const countRef = useRef(0);
countRef.current++; // UI won't update

✅ Use:

const [count, setCount] = useState(0);
setCount(count + 1); // UI updates

Refs bypass React’s rendering system—use them only for internal logic, not for UI-driven values.

2. When You Want to Share Data Between Components

Refs are local to a component. They do not create a shared or global state.

❌ Wrong:

Using a ref to share data between siblings.

✅ Use instead:

  • Props (parent → child)

  • Context API

  • State management (Redux, Zustand, Jotai)

  • URL state or server state

3. When You Want to Derive a Value

Derived values are better handled by:

  • useMemo

  • useEffect + state

  • Or computed inside the render function if cheap enough

❌ Don’t do this:

ref.current = computeSomething(a, b);

Derived values should stay declarative and predictable.

4. When You’re Storing Complex State

Refs allow silent mutation. When storing large or complex data structures, this can lead to:

  • Hard-to-debug issues

  • UI and internal logic are getting out of sync

  • Mutations happening outside React’s awareness

Use a state or a specialized state library** when holding complex or deeply nested data.

5. When You’re Trying to Optimize Prematurely

Sometimes developers reach for useRef thinking it’s “faster” because it avoids re-renders.

But avoiding re-renders isn’t always good:

  • React’s render cycle is optimized

  • Premature micro-optimizations can hurt maintainability

  • Refs should be used with a clear purpose

6. When You Can Use a Declarative Approach Instead

React excels when UI is described declaratively. Refs enable imperative patterns, which can make your code harder to follow.

❌ Imperative:

buttonRef.current.style.display = "none";

✅ Declarative:

{!isVisible && null}

Prefer the declarative approach unless necessary.

7. When You’re Trying to Replace Controlled Inputs

Avoid using refs for form element values as a substitute for controlled components.

❌ Don’t use:

const inputRef = useRef();
console.log(inputRef.current.value);

Why:

  • Harder to validate

  • Harder to sync with other fields

  • Harder to track form state

✅ Use:

const [value, setValue] = useState("");

8. When Simpler State Logic Will Do

If a ref usage feels “hacky” or hard to reason about, it probably is.

Examples:

  • Tracking toggles using refs

  • Controlling visibility via refs

  • Storing flags that actually affect UI

Use state instead—it is more transparent and predictable.

Key Takeaway

Use useRef sparingly and only in situations where:

  • You need a value to persist without triggering renders

  • You want to interact directly with the DOM

  • You need a mutable storage container for internal logic

If the value affects the UI, should be shared, or benefits from React’s reactive model—don't use refs.


Conclusion

useRef is one of React’s most versatile and misunderstood hooks. While it may seem simple—just a mutable object with a .current property—it unlocks powerful capabilities that go far beyond traditional state management.

In this tutorial, you learned how to:

  • Understand what useRef is and how it works

  • Access and control DOM elements imperatively

  • Store mutable values without causing re-renders

  • Track previous state or props

  • Manage timers, intervals, and event listeners

  • Persist values across renders

  • Avoid common mistakes and follow best practices

By now, you should have a solid grasp of when to use useRef, when not to use it, and how to apply it effectively in real-world scenarios.

useRef is not a replacement for state—it's an essential tool that complements React’s declarative model. When used thoughtfully, it helps you build more efficient, intuitive, and maintainable components.

With these examples and patterns, you're ready to confidently incorporate useRef into your React applications and improve your development workflow.

You can find the full source code on our GitHub.

That's just the basics. If you need more deep learning about React, you can take the following cheap course:

Thanks!