Refs
Access DOM nodes, store mutable values that persist across renders, and expose handles from child components.
Search across all documentation pages
Access DOM nodes, store mutable values that persist across renders, and expose handles from child components.
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
Quick-reference recipe card - copy-paste ready.
// DOM ref
const inputRef = useRef<HTMLInputElement>(null);
<input ref={inputRef} />
inputRef.current?.focus();
// Mutable value (no re-render on change)
const renderCount = useRef(0);
renderCount.current += 1;
// Ref callback (React 19 - supports cleanup)
<div ref={(node) => {
if (node) {
// setup
const observer = new ResizeObserver(() => { /* ... */ });
observer.observe(node);
return () => observer.disconnect(); // cleanup
}
}} />
// Forwarding ref (React 19 - ref is a regular prop)
function Input({ ref, ...props }: { ref?: React.Ref<HTMLInputElement> } & React.ComponentPropsWithoutRef<"input">) {
return <input ref={ref} {...props} />;
}When to reach for this: You need to interact with the DOM directly (focus, measure, scroll), integrate with a non-React library, or store a value that shouldn't trigger re-renders.
import { useRef } from "react";
function FocusButton() {
const inputRef = useRef<HTMLInputElement>(null);
return (
<div>
<input ref={inputRef} placeholder="Click the button to focus me" />
<button onClick={() => inputRef.current?.focus()}>Focus</button>
</div>
);
}The simplest ref pattern: grab a DOM node and call a method on it. inputRef.current is null until the <input> mounts, so use optional chaining (?.).
import { useRef } from "react";
function ScrollToSection() {
const sectionRef = useRef<HTMLDivElement>(null);
return (
<div>
<button onClick={() => sectionRef.current?.scrollIntoView({ behavior: "smooth" })}>
Jump to section
</button>
{/* ... lots of content ... */}
<div ref={sectionRef}>
<h2>Target Section</h2>
<p>You scrolled here!</p>
</div>
</div>
);
}Refs let you call any DOM API - scrollIntoView, getBoundingClientRect, play() on a video, etc.
import { useRef } from "react";
function ClickTracker() {
const clickCount = useRef(0);
function handleClick() {
clickCount.current += 1;
console.log(`Clicked ${clickCount.current} times`);
}
return <button onClick={handleClick}>Click me (check console)</button>;
}Unlike useState, updating ref.current does not trigger a re-render. This makes refs ideal for tracking values that don't need to appear in the UI - counters, flags, cached computations.
import { useRef } from "react";
function UncontrolledForm() {
const nameRef = useRef<HTMLInputElement>(null);
function handleSubmit(e: React.FormEvent) {
e.preventDefault();
alert(`Hello, ${nameRef.current?.value}`);
}
return (
<form onSubmit={handleSubmit}>
<input ref={nameRef} defaultValue="World" />
<button type="submit">Greet</button>
</form>
);
}Instead of wiring up onChange + useState, you can read the input's value directly from the DOM via ref.current.value. This is called an "uncontrolled" input.
import { useRef, useState } from "react";
function VideoPlayer({ src }: { src: string }) {
const videoRef = useRef<HTMLVideoElement>(null);
const [playing, setPlaying] = useState(false);
function togglePlay() {
if (playing) {
videoRef.current?.pause();
} else {
videoRef.current?.play();
}
setPlaying(!playing);
}
return (
<div>
<video ref={videoRef} src={src} width={320} />
<button onClick={togglePlay}>{playing ? "Pause" : "Play"}</button>
</div>
);
}Some DOM elements have imperative APIs (play, pause, showModal, close) that can only be called via a ref - there is no JSX prop equivalent.
"use client";
import { useRef, useState, useEffect } from "react";
export function AutoFocusSearch() {
const inputRef = useRef<HTMLInputElement>(null);
const [query, setQuery] = useState("");
const [results, setResults] = useState<string[]>([]);
const allItems = ["Apple", "Avocado", "Banana", "Blueberry", "Cherry", "Date", "Fig", "Grape"];
// Focus the input on mount
useEffect(() => {
inputRef.current?.focus();
}, []);
useEffect(() => {
if (query.trim()) {
setResults(
allItems.filter(item =>
item.toLowerCase().includes(query.toLowerCase())
)
);
} else {
setResults([]);
}
}, [query]);
return (
<div className="max-w-sm space-y-2 rounded border p-4">
<div className="flex gap-2">
<input
ref={inputRef}
value={query}
onChange={e => setQuery(e.target.value)}
placeholder="Search fruits..."
className="flex-1 rounded border px-3 py-1"
/>
<button
onClick={() => {
setQuery("");
inputRef.current?.focus();
}}
className="rounded bg-gray-200 px-3 py-1 text-sm"
>
Clear
</button>
</div>
{results.length > 0 && (
<ul className="list-inside list-disc text-sm">
{results.map(item => (
<li key={item}>{item}</li>
))}
</ul>
)}
{query && results.length === 0 && (
<p className="text-sm text-gray-400">No results for "{query}"</p>
)}
</div>
);
}
// --- Ref Callback with Cleanup (React 19) ---
export function MeasuredBox() {
const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
const measureRef = (node: HTMLDivElement | null) => {
if (!node) return;
const observer = new ResizeObserver(([entry]) => {
const { width, height } = entry.contentRect;
setDimensions({ width: Math.round(width), height: Math.round(height) });
});
observer.observe(node);
// React 19: return a cleanup function
return () => observer.disconnect();
};
return (
<div className="space-y-2">
<div
ref={measureRef}
className="resize overflow-auto rounded border bg-blue-50 p-4"
style={{ minWidth: 100, minHeight: 60 }}
>
Resize me!
</div>
<p className="text-xs text-gray-500">
{dimensions.width} x {dimensions.height}px
</p>
</div>
);
}What this demonstrates:
useRef to get a handle on an <input> element for programmatic focusinputRef.current?.focus()ResizeObserveruseRef(initialValue) returns a mutable object { current: initialValue } that persists for the full lifetime of the componentref prop, React sets .current to the DOM node after mounting and back to null on unmount.current does not trigger a re-render - this is the key difference from useStateuseEffect), which runs when the element unmounts or the ref changesref is a regular prop - no need for forwardRef wrapperuseRef:
| Parameter | Type | Description |
|---|---|---|
initialValue | T | Initial value for .current |
| Return | Type | Description |
|---|---|---|
ref | React.MutableRefObject<T> | Object with a mutable .current property |
| Type | Created By | Use Case |
|---|---|---|
React.RefObject<T> | useRef<T>(null) | DOM element references |
React.MutableRefObject<T> | useRef<T>(value) | Mutable instance variables (timers, previous values) |
React.Ref<T> | Callback or object | Accepting refs as props (union of callback ref and ref object) |
Storing previous value:
function usePrevious<T>(value: T): T | undefined {
const ref = useRef<T | undefined>(undefined);
useEffect(() => {
ref.current = value;
}, [value]);
return ref.current;
}
// Usage
const prevCount = usePrevious(count);Timer ref (avoiding stale closures):
function Stopwatch() {
const [elapsed, setElapsed] = useState(0);
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
function start() {
if (intervalRef.current) return;
intervalRef.current = setInterval(() => {
setElapsed(prev => prev + 1);
}, 1000);
}
function stop() {
if (intervalRef.current) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
}
useEffect(() => {
return () => stop(); // cleanup on unmount
}, []);
return (
<div>
<span>{elapsed}s</span>
<button onClick={start}>Start</button>
<button onClick={stop}>Stop</button>
</div>
);
}Forwarding refs in React 19 (no forwardRef needed):
interface FancyInputProps {
label: string;
ref?: React.Ref<HTMLInputElement>;
}
function FancyInput({ label, ref, ...props }: FancyInputProps) {
return (
<label>
{label}
<input ref={ref} {...props} />
</label>
);
}
// Parent
function Form() {
const inputRef = useRef<HTMLInputElement>(null);
return (
<>
<FancyInput label="Name" ref={inputRef} />
<button onClick={() => inputRef.current?.focus()}>Focus</button>
</>
);
}useImperativeHandle (exposing a custom API):
import { useRef, useImperativeHandle } from "react";
interface ModalHandle {
open: () => void;
close: () => void;
}
function Modal({ ref, children }: { ref?: React.Ref<ModalHandle>; children: React.ReactNode }) {
const dialogRef = useRef<HTMLDialogElement>(null);
useImperativeHandle(ref, () => ({
open: () => dialogRef.current?.showModal(),
close: () => dialogRef.current?.close(),
}));
return <dialog ref={dialogRef}>{children}</dialog>;
}
// Parent
function App() {
const modalRef = useRef<ModalHandle>(null);
return (
<>
<button onClick={() => modalRef.current?.open()}>Open</button>
<Modal ref={modalRef}>
<p>Hello!</p>
<button onClick={() => modalRef.current?.close()}>Close</button>
</Modal>
</>
);
}// DOM ref - pass null as initial value, type the element
const divRef = useRef<HTMLDivElement>(null);
const canvasRef = useRef<HTMLCanvasElement>(null);
// Mutable ref - non-null initial value
const countRef = useRef<number>(0); // MutableRefObject<number>
// Accepting a ref prop in React 19
interface Props {
ref?: React.Ref<HTMLInputElement>;
}
// Typing useImperativeHandle
interface Handle {
scrollToTop: () => void;
}
useImperativeHandle(ref, (): Handle => ({
scrollToTop: () => window.scrollTo(0, 0),
}));Reading ref during render - inputRef.current is null during the first render because the DOM node doesn't exist yet. Fix: Access refs in event handlers, useEffect, or after a null check.
Ref changes don't trigger re-renders - Updating ref.current will not cause the component to re-render or reflect changes in JSX. Fix: If you need the UI to update, use useState instead. Use refs only for values the render output doesn't depend on.
forwardRef is deprecated in React 19 - React.forwardRef still works but is no longer necessary. Fix: Accept ref as a regular prop: function MyComp({ ref }: { ref?: React.Ref<HTMLElement> }).
Assigning ref to a conditional element - If the element with ref is conditionally rendered, .current will be null when the element is hidden. Fix: Always null-check before using: ref.current?.focus().
Ref callback firing twice in StrictMode - In development with StrictMode, ref callbacks fire with null then the node, simulating mount/unmount/remount. Fix: This is expected - ensure your ref callback handles null gracefully. In React 19, return a cleanup function instead of checking for null.
| Alternative | Use When | Don't Use When |
|---|---|---|
useState | The value needs to trigger re-renders when it changes | You're storing a timer ID, previous value, or DOM node |
document.getElementById | Quick prototype outside React | Production React code (breaks component encapsulation) |
| Data attributes + CSS | You need to toggle styles based on state | You need programmatic DOM access (focus, scroll, measure) |
useImperativeHandle | You want to expose a limited API from a child component | You just need the raw DOM node |
useRef returns a mutable object { current: value } that persists across renders without causing re-renders. Use it for DOM access (focus, scroll, measure), storing timer IDs, and keeping mutable values that the UI doesn't depend on.
useState triggers a re-render when updated - use for values the UI displaysuseRef does not trigger re-renders - use for values like timer IDs, previous values, or DOM nodesDOM refs are populated after React mounts the element. During render, the DOM node doesn't exist yet. Access refs in event handlers, useEffect, or behind a null check: ref.current?.focus().
No. In React 19, ref is a regular prop. Accept it directly:
function Input({ ref, ...props }: { ref?: React.Ref<HTMLInputElement> }) {
return <input ref={ref} {...props} />;
}A ref callback is a function passed to the ref prop. React calls it with the DOM node on mount and null on unmount. In React 19, it can return a cleanup function. Use it when you need setup/teardown logic tied to the DOM node (like ResizeObserver).
const timerRef = useRef<ReturnType<typeof setInterval> | null>(null);
timerRef.current = setInterval(() => { ... }, 1000);
// Later:
clearInterval(timerRef.current!);This avoids stale closure issues since ref.current always points to the latest value.
It lets you customize the value exposed when a parent uses a ref on your component. Instead of exposing the raw DOM node, you expose a limited API like { open(), close() }. Use sparingly - prefer props for most communication.
Yes. Increment ref.current in the component body:
const renderCount = useRef(0);
renderCount.current += 1;Since ref changes don't trigger re-renders, this won't cause an infinite loop.
React's StrictMode simulates unmount/remount in development, so the ref callback fires with null then the node. This is expected. In React 19, return a cleanup function from the ref callback instead of checking for null.
function usePrevious<T>(value: T): T | undefined {
const ref = useRef<T | undefined>(undefined);
useEffect(() => { ref.current = value; }, [value]);
return ref.current;
}The ref stores the old value because useEffect runs after render.
Reviewed by Chris St. John·Last updated Jul 16, 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥