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.currentin 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 |