Skip to content

React Refs Explained — Accessing DOM Elements and Mutable Values

DodaTech Updated 2026-06-28 6 min read

In this tutorial, you will learn about React Refs Explained. We cover key concepts, practical examples, and best practices to help you master this topic.

React refs provide a way to access DOM elements directly and store mutable values that persist across renders without causing re-renders.

What You'll Learn

  • What refs are and when to use them
  • How to use useRef for DOM element access
  • How to store mutable values without re-renders
  • How to forward refs to child components
  • How to use callback refs for dynamic refs

Why It Matters

Refs are the escape hatch from React's declarative world. They let you do imperative things like focus inputs, measure elements, and integrate with third-party libraries that need direct DOM access.

Real-World Use

Durga Antivirus Pro uses refs to focus the search input on navigation, measure chart container dimensions for responsive graphs, and store Websocket instances that should not trigger re-renders.

flowchart LR
    A[Component] -->|useRef| B[Ref Object]
    B --> C[DOM Element]
    B --> D[Mutable Value]
    C --> E[Focus / Measure / Animate]
    D --> F[Persist Across Renders]
    style A fill:#3b82f6,color:#fff

Accessing DOM Elements

The most common use for refs:

import { useRef, useEffect } from "react";

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

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

  return (
    <div>
      <label htmlFor="search">Search:</label>
      <input
        ref={inputRef}
        id="search"
        type="text"
        placeholder="Type to search..."
      />
    </div>
  );
}

Expected output: When the component renders, the input field automatically receives focus. The cursor blinks inside it.

useRef returns a mutable object with a .current property. Initially set to the argument passed to useRef (null in this case). When the ref is attached to a DOM element via the ref attribute, React sets ref.current to that DOM node.

Mutable Values That Persist

Refs can store any value without causing re-renders:

import { useRef, useState } from "react";

function Stopwatch() {
  const [time, setTime] = useState(0);
  const intervalRef = useRef(null);
  const startTimeRef = useRef(null);

  const start = () => {
    if (intervalRef.current) return; // Already running

    startTimeRef.current = Date.now() - time * 1000;
    intervalRef.current = setInterval(() => {
      const elapsed = Math.floor((Date.now() - startTimeRef.current) / 1000);
      setTime(elapsed);
    }, 100);
  };

  const stop = () => {
    if (intervalRef.current) {
      clearInterval(intervalRef.current);
      intervalRef.current = null;
    }
  };

  const reset = () => {
    stop();
    setTime(0);
    startTimeRef.current = null;
  };

  return (
    <div>
      <p>Elapsed: {time}s</p>
      <button onClick={start}>Start</button>
      <button onClick={stop}>Stop</button>
      <button onClick={reset}>Reset</button>
    </div>
  );
}

Expected output: A stopwatch that starts, stops, and resets. The interval ID is stored in a ref and does not cause re-renders when changed.

Unlike state, updating a ref's .current property does NOT trigger a re-render. This makes refs ideal for storing timers, subscriptions, and other imperative handles.

forwardRef

Pass refs through to child components:

import { forwardRef, useRef } from "react";

const FancyInput = forwardRef((props, ref) => {
  return (
    <div className="fancy-input">
      <label>{props.label}</label>
      <input ref={ref} className="input" {...props} />
    </div>
  );
});

FancyInput.displayName = "FancyInput";

function ParentForm() {
  const nameRef = useRef(null);
  const emailRef = useRef(null);

  const handleSubmit = (e) => {
    e.preventDefault();
    alert(`Name: ${nameRef.current.value}, Email: ${emailRef.current.value}`);
  };

  const focusFirst = () => {
    nameRef.current.focus();
  };

  return (
    <form onSubmit={handleSubmit}>
      <FancyInput ref={nameRef} label="Name" placeholder="Enter name" />
      <FancyInput ref={emailRef} label="Email" type="email" placeholder="Enter email" />
      <button type="button" onClick={focusFirst}>Focus Name</button>
      <button type="submit">Submit</button>
    </form>
  );
}

Expected output: Two fancy inputs. Clicking "Focus Name" focuses the name field. Submit reads values from the refs.

forwardRef lets a component accept a ref prop and forward it to a DOM element inside it. Without it, the ref prop would not work on custom components.

Callback Refs

For dynamic ref management:

import { useState, useCallback } from "react";

function MeasureExample() {
  const [height, setHeight] = useState(0);

  const measuredRef = useCallback(node => {
    if (node !== null) {
      setHeight(node.getBoundingClientRect().height);
    }
  }, []);

  const [show, setShow] = useState(true);

  return (
    <div>
      <button onClick={() => setShow(!show)}>
        {show ? "Hide" : "Show"} box
      </button>
      {show && (
        <div ref={measuredRef} style={{
          padding: "20px",
          background: "#3b82f6",
          color: "white",
          borderRadius: "8px"
        }}>
          <p>This box is {height}px tall</p>
        </div>
      )}
    </div>
  );
}

Expected output: When the box renders, its height is measured and displayed. When re-rendered (e.g., content changes), the callback fires again.

Callback refs give finer control. React calls the callback with the DOM node when it mounts and with null when it unmounts. This is useful for measuring, animations, and dynamic ref logic.

Managing Imperative Handles

Expose specific methods, not the whole DOM node:

import { forwardRef, useImperativeHandle, useRef } from "react";

const VideoPlayer = forwardRef(({ src }, ref) => {
  const videoRef = useRef(null);

  useImperativeHandle(ref, () => ({
    play() {
      videoRef.current.play();
    },
    pause() {
      videoRef.current.pause();
    },
    stop() {
      videoRef.current.pause();
      videoRef.current.currentTime = 0;
    },
    getDuration() {
      return videoRef.current.duration;
    },
  }), []);

  return (
    <video ref={videoRef} src={src} width="400" controls />
  );
});

VideoPlayer.displayName = "VideoPlayer";

function VideoController() {
  const playerRef = useRef(null);

  return (
    <div>
      <VideoPlayer ref={playerRef} src="https://example.com/video.mp4" />
      <button onClick={() => playerRef.current.play()}>Play</button>
      <button onClick={() => playerRef.current.pause()}>Pause</button>
      <button onClick={() => playerRef.current.stop()}>Stop</button>
    </div>
  );
}

Expected output: Control buttons that play, pause, and stop the video through the exposed imperative handle.

useImperativeHandle lets you customize the instance value exposed to parent refs. Instead of exposing the entire DOM element, you expose only the methods you want.

Common Mistakes

  1. Overusing refs when state would work — Use refs only for imperative operations. If you can achieve the same with state, do that.

  2. Reading ref.current in render — Refs are not reactive. Reading ref.current during render gives the value at render time, which may be stale.

  3. Setting refs in render — Setting ref.current during render is a side effect and belongs in useEffect.

  4. Forgetting forwardRef for component refs — Custom components do not pass ref through unless wrapped in forwardRef.

  5. Refs as a replacement for state — Refs do not trigger re-renders. If the UI needs to update when the value changes, use state.

Practice Questions

  1. What is a ref in React? A mutable object with a .current property that persists across renders without triggering re-renders.

  2. How do you access a DOM element with useRef? Pass the ref object's ref attribute to the JSX element: <input ref={myRef} />.

  3. What does forwardRef do? It lets a custom component accept and forward a ref to one of its child DOM elements.

  4. What is useImperativeHandle used for? To restrict the methods exposed when using forwardRef, so the parent can only call specific methods.

  5. When should you use a callback ref instead of useRef? When you need to run code when the ref attaches or detaches, or when you need to measure a dynamic element.

Challenge

Build a DraggablePanel component that uses refs for the drag handle and panel elements. Implement drag logic using mouse events. Store the drag state (isDragging, offset) in refs to avoid unnecessary re-renders during drag. Only update state when the drag ends.

FAQ

Can I use refs in function components without forwardRef?

Yes, only when the ref targets a DOM element or a native component. Custom components need forwardRef.

What is the difference between useRef and createRef?

useRef creates a ref once per component mount. createRef creates a new ref every render.

Can a ref hold a component instance?

With class components, yes. With function components, useImperativeHandle to control what is exposed.

When does ref.current update?

React sets ref.current during the commit phase, after DOM mutations. It is safe to read after mount.

Are refs like state in terms of performance?

Refs are cheaper because updating them does not trigger re-renders. Use refs for values that do not affect the UI.

Mini Project

Build a RichTextEditor component using a contentEditable div. Use a ref to access the div for formatting commands (bold, italic, underline). Store the history of edits in a ref to implement undo/redo. Measure the editor height with a callback ref and adjust the container. Use forwardRef to expose getContent and setContent methods to the parent.

What's Next

Continue with context and reducers:

React Context, React Reducers, React Hooks

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro