TechLead

useRef & DOM Manipulation

Master React refs for DOM access, imperative handles, and mutable values that persist across renders.

useRef & DOM Manipulation

useRef is a React hook that returns a mutable ref object whose .current property persists across renders β€” without causing re-renders when it changes. It's your escape hatch for imperative DOM operations, storing previous values, and managing external library instances.

πŸ”‘ Key Insight

useState is for values that affect the UI (re-render on change). useRef is for values that should not trigger re-renders β€” DOM nodes, timers, previous values, and instance variables.

1. Accessing DOM Elements

The most common use case β€” attach a ref to a JSX element and interact with the underlying DOM node.

import { useRef, useEffect } from "react";

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

  useEffect(() => {
    // Focus the input on mount
    inputRef.current.focus();
  }, []);

  return <input ref={inputRef} placeholder="I auto-focus!" />;
}

2. Scrolling to Elements

Use refs to scroll specific elements into view β€” useful in chat UIs, form errors, and long lists.

function ChatMessages({ messages }) {
  const bottomRef = useRef(null);

  useEffect(() => {
    // Scroll to bottom when new messages arrive
    bottomRef.current?.scrollIntoView({ behavior: "smooth" });
  }, [messages]);

  return (
    <div className="overflow-y-auto h-96">
      {messages.map((msg) => (
        <div key={msg.id} className="p-2">{msg.text}</div>
      ))}
      <div ref={bottomRef} />
    </div>
  );
}

3. Storing Mutable Values (Timers, Intervals)

Refs are perfect for values that need to persist across renders but shouldn't trigger re-renders β€” like timer IDs.

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

function Stopwatch() {
  const [seconds, setSeconds] = useState(0);
  const [running, setRunning] = useState(false);
  const intervalRef = useRef(null);

  const start = () => {
    if (running) return;
    setRunning(true);
    intervalRef.current = setInterval(() => {
      setSeconds((s) => s + 1);
    }, 1000);
  };

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

  const reset = () => {
    stop();
    setSeconds(0);
  };

  // Cleanup on unmount
  useEffect(() => {
    return () => clearInterval(intervalRef.current);
  }, []);

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

4. Tracking Previous Values

A common pattern β€” use a ref to remember the previous value of a prop or state.

import { useRef, useEffect } from "react";

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

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

  return ref.current; // Returns value from previous render
}

// Usage
function PriceDisplay({ price }) {
  const prevPrice = usePrevious(price);
  const direction = price > prevPrice ? "πŸ“ˆ" : price < prevPrice ? "πŸ“‰" : "➑️";

  return (
    <span>
      {direction} ${price}
    </span>
  );
}

5. Uncontrolled Form Inputs

Instead of tracking every keystroke in state, use refs for forms where you only need the final value.

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

  const handleSubmit = (e) => {
    e.preventDefault();
    const data = {
      name: nameRef.current.value,
      email: emailRef.current.value,
    };
    console.log("Submitted:", data);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input ref={nameRef} placeholder="Name" defaultValue="" />
      <input ref={emailRef} placeholder="Email" defaultValue="" />
      <button type="submit">Submit</button>
    </form>
  );
}

6. forwardRef β€” Passing Refs to Child Components

By default, refs don't pass through to child components. Use forwardRef to expose a child's DOM node to a parent.

import { forwardRef, useRef } from "react";

// Child component that accepts a ref
const FancyInput = forwardRef(function FancyInput(props, ref) {
  return (
    <input
      ref={ref}
      className="border-2 border-purple-500 rounded p-2"
      {...props}
    />
  );
});

// Parent component that uses the ref
function Form() {
  const inputRef = useRef(null);

  return (
    <div>
      <FancyInput ref={inputRef} placeholder="Type here..." />
      <button onClick={() => inputRef.current.focus()}>
        Focus Input
      </button>
    </div>
  );
}

7. useImperativeHandle β€” Custom Ref APIs

Control exactly which methods a parent can call on a child's ref. This is great for reusable components with imperative APIs.

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

const Modal = forwardRef(function Modal({ title, children }, ref) {
  const [isOpen, setIsOpen] = useState(false);

  // Expose a clean API to the parent
  useImperativeHandle(ref, () => ({
    open: () => setIsOpen(true),
    close: () => setIsOpen(false),
    toggle: () => setIsOpen((prev) => !prev),
  }));

  if (!isOpen) return null;

  return (
    <div className="fixed inset-0 bg-black/50 flex items-center justify-center">
      <div className="bg-white rounded p-6">
        <h2>{title}</h2>
        {children}
        <button onClick={() => setIsOpen(false)}>Close</button>
      </div>
    </div>
  );
});

// Parent controls the modal imperatively
function App() {
  const modalRef = useRef(null);

  return (
    <div>
      <button onClick={() => modalRef.current.open()}>Open Modal</button>
      <Modal ref={modalRef} title="Settings">
        <p>Modal content here</p>
      </Modal>
    </div>
  );
}

8. Integrating Third-Party Libraries

Refs bridge React's declarative world with imperative DOM libraries like charts, maps, or video players.

import { useRef, useEffect } from "react";
import Chart from "chart.js/auto";

function SalesChart({ data }) {
  const canvasRef = useRef(null);
  const chartRef = useRef(null);

  useEffect(() => {
    // Create chart on mount
    chartRef.current = new Chart(canvasRef.current, {
      type: "line",
      data: {
        labels: data.map((d) => d.month),
        datasets: [{ label: "Sales", data: data.map((d) => d.value) }],
      },
    });

    // Cleanup: destroy chart on unmount
    return () => {
      chartRef.current.destroy();
    };
  }, [data]);

  return <canvas ref={canvasRef} />;
}

9. Callback Refs β€” Dynamic Ref Assignment

Instead of a ref object, pass a function. React calls it with the DOM node when attached and null when detached β€” useful for measuring elements or handling dynamic lists.

import { useCallback, useState } from "react";

function MeasuredBox() {
  const [dimensions, setDimensions] = useState(null);

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

  return (
    <div>
      <div ref={measuredRef} className="p-8 bg-blue-100 rounded">
        Measure me!
      </div>
      {dimensions && (
        <p>
          Size: {Math.round(dimensions.width)} x {Math.round(dimensions.height)}
        </p>
      )}
    </div>
  );
}

10. Ref Patterns to Avoid

❌ Anti-Patterns

  • β€’ Don't read/write refs during render β€” only in effects or event handlers. Reading refs during render makes the component unpredictable.
  • β€’ Don't use refs to replace state β€” if changing a value should update the UI, use useState.
  • β€’ Don't overuse refs for DOM manipulation β€” prefer React's declarative model (conditional rendering, className, style props).
  • β€’ Don't access ref.current in the render phase of a parent for a child's ref β€” the child may not have mounted yet.
// ❌ BAD β€” reading ref during render
function Bad() {
  const ref = useRef(0);
  ref.current += 1; // Side effect during render!
  return <p>{ref.current}</p>;
}

// βœ… GOOD β€” reading ref in an event handler
function Good() {
  const countRef = useRef(0);
  const [display, setDisplay] = useState(0);

  const handleClick = () => {
    countRef.current += 1;
    setDisplay(countRef.current);
  };

  return <button onClick={handleClick}>Clicked: {display}</button>;
}

πŸ“‹ Quick Reference: When to Use useRef

Use Case Hook Re-renders?
UI-affecting data useState βœ… Yes
DOM element access useRef ❌ No
Timer/interval IDs useRef ❌ No
Previous value tracking useRef ❌ No
Third-party lib instance useRef ❌ No