Skip to content

React useRef Hook — Complete Guide with Examples

DodaTech 9 min read

In this tutorial, you'll learn about React useRef Hook. We cover key concepts, practical examples, and best practices.

The React useRef hook creates a mutable reference that persists across renders without causing re-renders when its value changes, making it essential for DOM access and storing non-reactive data.

What You'll Learn

Master the useRef hook — access DOM elements directly, store mutable values that survive re-renders, track render counts, and understand when to use refs instead of state.

Why It Matters

Every React developer hits a wall: "How do I focus an input?" or "How do I store a timer ID without causing re-renders?" useRef solves these. It's the tool for values that need to persist but don't need to trigger UI updates.

Real-World Use

Focusing a search input on page load, storing a WebSocket connection reference, tracking how many times a component rendered, or holding an interval ID for a stopwatch.

What is useRef?

The useRef hook returns a mutable object with a .current property. Unlike state, updating a ref does not trigger a re-render. The ref object lives for the entire lifetime of the component.

import { useRef } from "react";

function SimpleExample() {
  // Ref starts as { current: null }
  const ref = useRef(null);

  console.log(ref); // { current: null }

  return <p>Check the console</p>;
}

Think of a ref as a drawer built into your component. You can put things in the drawer, take them out, or swap them — but the drawer itself doesn't make any noise. Nobody knows you changed what's inside. This is different from state, which is like a loudspeaker — every change announces itself and causes a re-render.

Accessing DOM Elements

The most common use case for useRef is getting direct access to a DOM element. Instead of using document.getElementById, you attach a ref to a JSX element.

import { useRef, useEffect } from "react";

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

  useEffect(() => {
    // Automatically focus the input when component mounts
    inputRef.current.focus();
  }, []);

  return (
    <div style={{ padding: "20px" }}>
      <input
        ref={inputRef}
        type="text"
        placeholder="I get auto-focused!"
        style={{ padding: "8px", fontSize: "16px" }}
      />
    </div>
  );
}

How it works:

  1. useRef(null) creates a ref with initial value { current: null }.
  2. <input ref={inputRef} /> — React assigns the real DOM node to inputRef.current.
  3. Inside useEffect, inputRef.current.focus() calls the native DOM focus method.
  4. We use useEffect here because the DOM element doesn't exist until after the component mounts.

Expected output: When this component renders, the input field automatically receives focus (cursor appears inside it).

Storing Mutable Values

Refs are perfect for values that change but should not cause re-renders. A classic example is storing a timer ID.

import { useState, useRef } from "react";

function Stopwatch() {
  const [count, setCount] = useState(0);
  const intervalRef = useRef(null);

  function start() {
    if (intervalRef.current !== null) return; // Already running
    intervalRef.current = setInterval(() => {
      setCount(prev => prev + 1);
    }, 1000);
  }

  function stop() {
    if (intervalRef.current !== null) {
      clearInterval(intervalRef.current);
      intervalRef.current = null;
    }
  }

  function reset() {
    stop();
    setCount(0);
  }

  return (
    <div style={{ padding: "20px" }}>
      <h2>Stopwatch: {count}s</h2>
      <button onClick={start} style={{ marginRight: "8px" }}>Start</button>
      <button onClick={stop} style={{ marginRight: "8px" }}>Stop</button>
      <button onClick={reset}>Reset</button>
    </div>
  );
}

Why not use state for the interval ID? Because updating state causes a re-render. The interval ID is just a number we need to store — it's not UI data. Changing it shouldn't flash the screen. A ref stores it silently.

Tracking Renders

You can use a ref to count how many times a component renders without causing infinite loops.

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

function RenderCounter() {
  const [text, setText] = useState("");
  const renderCount = useRef(0);

  // Increment ref on every render
  renderCount.current += 1;

  return (
    <div style={{ padding: "20px" }}>
      <input
        value={text}
        onChange={e => setText(e.target.value)}
        placeholder="Type something..."
        style={{ padding: "8px", fontSize: "16px" }}
      />
      <p>You typed: {text}</p>
      <p style={{ color: "#666" }}>
        Component rendered {renderCount.current} times
      </p>
    </div>
  );
}

Expected output: Every time you type in the input, the render counter increments. This works because setting state (via setText) triggers a re-render, and the ref keeps counting. If we used useState for the render count, we'd get an infinite loop — updating state would trigger a re-render, which would update state again, and so on.

useRef vs useState

Aspect useRef useState
Triggers re-render No Yes
Persists across renders Yes Yes
Syntax ref.current = value setState(value)
Reading value ref.current (synchronous) State value (available next render)
Best for DOM refs, timers, mutable data UI data, user input
Initial value useRef(initial) useState(initial)

What is the difference between useRef and useState?

useRef persists values across renders without causing re-renders when the value changes, making it ideal for DOM references and non-UI data. useState triggers a re-render every time the value updates, which is necessary for data displayed on screen.

Previous State with useRef

You can combine useState and useRef to track the previous value of a state variable — useful for comparisons.

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

function PreviousValueTracker() {
  const [count, setCount] = useState(0);
  const prevCountRef = useRef(null);

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

  const prevCount = prevCountRef.current;

  return (
    <div style={{ padding: "20px" }}>
      <h2>Current: {count}</h2>
      <h2 style={{ color: "#666" }}>
        Previous: {prevCount !== null ? prevCount : "N/A"}
      </h2>
      <button onClick={() => setCount(c => c + 1)}>Increment</button>
    </div>
  );
}

How it works: The useEffect runs after the render, so prevCountRef.current always holds the value from the previous render cycle. On the first render, it's null.

useRef for Video and Media

Refs shine when controlling media elements like video, audio, or canvas.

import { useRef, useState } from "react";

function VideoPlayer({ src }) {
  const videoRef = useRef(null);
  const [isPlaying, setIsPlaying] = useState(false);

  function togglePlay() {
    const video = videoRef.current;
    if (video.paused) {
      video.play();
      setIsPlaying(true);
    } else {
      video.pause();
      setIsPlaying(false);
    }
  }

  return (
    <div style={{ padding: "20px" }}>
      <video
        ref={videoRef}
        src={src}
        width="400"
        style={{ display: "block", marginBottom: "10px" }}
      />
      <button onClick={togglePlay}>
        {isPlaying ? "Pause" : "Play"}
      </button>
    </div>
  );
}

This pattern is used in media players, video conferencing apps, and security camera interfaces built by tools like Doda Browser for real-time video playback.

Common Errors with useRef

1. Reading ref.current too early

Accessing ref.current before the component mounts returns the initial value (usually null).

function BadRef() {
  const inputRef = useRef(null);
  // ❌ inputRef.current is null here — DOM not ready yet
  inputRef.current.focus();
  return <input ref={inputRef} />;
}

Fix: Access refs inside useEffect or event handlers — after the DOM is mounted.

2. Using ref instead of state for UI data

Refs don't trigger re-renders. If you store UI data in a ref, the screen won't update.

function BadCounter() {
  const countRef = useRef(0);
  function increment() {
    countRef.current += 1; // Updates ref but NO re-render
  }
  return <p>{countRef.current}</p>; // Never updates on screen
}

Fix: Use useState for anything displayed on screen.

3. Forgetting refs persist across renders

Unlike local variables (which reset every render), refs keep their value. This is a feature, but can be surprising.

4. Using refs for prop-like values

Refs should not replace props for passing data to child components. Use props for that.

5. Mutating refs inside render without thought

Mutating a ref during rendering is fine for things like render counts, but avoid complex side effects in the render phase.

6. Not cleaning up ref values on unmount

If a ref holds a timer, WebSocket, or subscription, clear it in a useEffect cleanup.

7. Overusing useRef when useState would be simpler

Ask yourself: "Does this change need to be reflected on screen?" If yes, use state.

Practice Questions

  1. What does useRef return? A mutable object with a .current property initialized to the passed argument.

  2. Does updating a ref cause a re-render? No. Changing ref.current does not trigger a re-render. This is the key difference from useState.

  3. When can you safely access ref.current for a DOM element? After the component has mounted — inside useEffect or an event handler.

  4. Why not use document.querySelector instead of useRef? useRef is tied to the component lifecycle, works with React's virtual DOM, and avoids fragile CSS selectors.

  5. Can you pass a ref to a custom component? Yes, but the custom component must use forwardRef to accept the ref and attach it to a DOM element.

Challenge

Build a measuring tool component that:

  • Renders a <div> with dynamic text content entered by the user
  • Uses a ref to measure the rendered width/height of the div
  • Displays the measurements in real-time
  • Updates measurements when the text changes
// Starter code — complete the implementation
function MeasurementTool() {
  const [text, setText] = useState("");
  const divRef = useRef(null);
  const [dimensions, setDimensions] = useState({ width: 0, height: 0 });

  // Your code here
  // 1. Use useEffect to measure the div after render
  // 2. Update dimensions state with ref.current measurements
  // 3. Handle the case where divRef.current is null

  return (
    <div style={{ padding: "20px" }}>
      <input value={text} onChange={e => setText(e.target.value)} />
      <div ref={divRef} style={{ display: "inline-block", border: "1px solid" }}>
        {text || "Type to see me"}
      </div>
      <p>Width: {dimensions.width}px, Height: {dimensions.height}px</p>
    </div>
  );
}

Real-World Task: Form Auto-Save

Build a form that auto-saves draft content to localStorage every 5 seconds using a ref for the interval:

function AutoSaveForm() {
  const [formData, setFormData] = useState({ title: "", content: "" });
  const saveIntervalRef = useRef(null);

  useEffect(() => {
    saveIntervalRef.current = setInterval(() => {
      localStorage.setItem("draft", JSON.stringify(formData));
      console.log("Draft saved at", new Date().toLocaleTimeString());
    }, 5000);
    return () => clearInterval(saveIntervalRef.current);
  }, [formData]);

  return (
    <div style={{ padding: "20px" }}>
      <input
        value={formData.title}
        onChange={e => setFormData(f => ({ ...f, title: e.target.value }))}
        placeholder="Title"
      />
      <textarea
        value={formData.content}
        onChange={e => setFormData(f => ({ ...f, content: e.target.value }))}
        placeholder="Content"
      />
    </div>
  );
}

Learning Path

graph LR
    A[React Basics] --> B[useState Hook]
    B --> C[useEffect Hook]
    C --> D[Context API]
    D --> E[useReducer Hook]
    E --> F[Custom Hooks]
    F --> G[useRef Hook]
    G --> H[React Router]
    G --> I[Performance Optimization]
    style G fill:#ff6b6b,stroke:#333,stroke-width:2px,color:#fff
  • React Tutorial for Beginners — start here if you're new
  • React Hooks Complete Guide — overview of all hooks
  • Learn about the useState hook for state management
  • Master the useEffect hook for data fetching
  • Explore the useReducer hook for advanced logic
Can I use useRef for form inputs?

Yes, useRef can access form input values without re-rendering on each keystroke (uncontrolled components). Use ref.current.value to read the value when needed, like on form submit.

Does useRef cause infinite loops?

No. Unlike state, updating a ref does not trigger a re-render, so you cannot create an infinite loop with useRef alone. This makes it safe for tracking render counts or storing timers.

What is forwardRef?

forwardRef is a React function that lets parent components pass refs to child components. By default, function components don't accept refs — forwardRef wraps the component to forward the ref to a DOM element inside it.


Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro. Last updated: June 2026.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro