Refs
Accede a nodos del DOM, almacena valores mutables que persisten entre renders y expón handles desde componentes hijos.
Busca en todas las páginas de la documentación
Accede a nodos del DOM, almacena valores mutables que persisten entre renders y expón handles desde componentes hijos.
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
Tarjeta de receta de referencia rápida - lista para copiar y pegar.
// Ref al DOM
const inputRef = useRef<HTMLInputElement>(null);
<input ref={inputRef} />
inputRef.current?.focus();
// Valor mutable (sin re-render al cambiar)
const renderCount = useRef(0);
renderCount.current += 1;
// Ref callback (React 19 - admite limpieza)
<div ref={(node) => {
if (node) {
// configuración
const observer = new ResizeObserver(() => { /* ... */ });
observer.observe(node);
return () => observer.disconnect(); // limpieza
}
}} />
// Reenvío de ref (React 19 - ref es una prop normal)
function Input({ ref, ...props }: { ref?: React.Ref<HTMLInputElement> } & React.ComponentPropsWithoutRef<"input">) {
return <input ref={ref} {...props} />;
}Cuándo recurrir a esto: Necesitas interactuar directamente con el DOM (enfocar, medir, desplazar), integrarte con una librería ajena a React o almacenar un valor que no debería provocar re-renders.
import { useRef } from "react";
function FocusButton() {
const inputRef = useRef<HTMLInputElement>(null);
return (
<div>
<input ref={inputRef} placeholder="Haz clic en el botón para enfocarme" />
<button onClick={() => inputRef.current?.focus()}>Enfocar</button>
</div>
);
}El patrón de ref más simple: tomar un nodo del DOM y llamar a un método sobre él. inputRef.current es null hasta que el <input> se monta, así que usa el encadenamiento opcional (?.).
import { useRef } from "react";
function ScrollToSection() {
const sectionRef = useRef<HTMLDivElement>(null);
return (
<div>
<button onClick={() => sectionRef.current?.scrollIntoView({ behavior: "smooth" })}>
Ir a la sección
</button>
{/* ... mucho contenido ... */}
<div ref={sectionRef}>
<h2>Sección Objetivo</h2>
<p>¡Te desplazaste hasta aquí!</p>
</div>
</div>
);
}Los refs te permiten llamar a cualquier API del DOM - scrollIntoView, getBoundingClientRect, play() en un vídeo, etc.
import { useRef } from "react";
function ClickTracker() {
const clickCount = useRef(0);
function handleClick() {
clickCount.current += 1;
console.log(`Clics: ${clickCount.current} veces`);
}
return <button onClick={handleClick}>Haz clic (revisa la consola)</button>;
}A diferencia de useState, actualizar ref.current no provoca un nuevo render. Esto hace que los refs sean ideales para rastrear valores que no necesitan aparecer en la UI - contadores, banderas, cálculos en caché.
import { useRef } from "react";
function UncontrolledForm() {
const nameRef = useRef<HTMLInputElement>(null);
function handleSubmit(e: React.FormEvent) {
e.preventDefault();
alert(`Hola, ${nameRef.current?.value}`);
}
return (
<form onSubmit={handleSubmit}>
<input ref={nameRef} defaultValue="Mundo" />
<button type="submit">Saludar</button>
</form>
);
}En lugar de conectar onChange + useState, puedes leer el valor del input directamente desde el DOM a través de ref.current.value. Esto se conoce como un input "no controlado".
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 ? "Pausar" : "Reproducir"}</button>
</div>
);
}Algunos elementos del DOM tienen APIs imperativas (play, pause, showModal, close) que solo pueden llamarse a través de un ref - no existe una prop equivalente en JSX.
"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"];
// Enfocar el input al montar
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="Buscar frutas..."
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"
>
Limpiar
</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">Sin resultados para "{query}"</p>
)}
</div>
);
}
// --- Ref Callback con Limpieza (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: devuelve una función de limpieza
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 }}
>
¡Redimensióname!
</div>
<p className="text-xs text-gray-500">
{dimensions.width} x {dimensions.height}px
</p>
</div>
);
}Qué demuestra esto:
useRef para obtener un handle sobre un elemento <input> para enfocarlo de forma programáticainputRef.current?.focus()ResizeObserveruseRef(initialValue) devuelve un objeto mutable { current: initialValue } que persiste durante toda la vida del componenteref de un elemento JSX, React establece .current en el nodo del DOM tras el montaje y lo devuelve a null en el desmontaje.current no provoca un nuevo render - esta es la diferencia clave con useStateuseEffect), que se ejecuta cuando el elemento se desmonta o el ref cambiaref es una prop normal - no se necesita el envoltorio forwardRefuseRef:
| Parámetro | Tipo | Descripción |
|---|---|---|
initialValue | T | Valor inicial para .current |
| Retorno | Tipo | Descripción |
|---|---|---|
ref | React.MutableRefObject<T> | Objeto con una propiedad .current mutable |
| Tipo | Creado Por | Caso de Uso |
|---|---|---|
React.RefObject<T> | useRef<T>(null) | Referencias a elementos del DOM |
React.MutableRefObject<T> | useRef<T>(value) | Variables de instancia mutables (temporizadores, valores previos) |
React.Ref<T> | Callback u objeto | Aceptar refs como props (unión de ref callback y objeto ref) |
Almacenar el valor previo:
function usePrevious<T>(value: T): T | undefined {
const ref = useRef<T | undefined>(undefined);
useEffect(() => {
ref.current = value;
}, [value]);
return ref.current;
}
// Uso
const prevCount = usePrevious(count);Ref de temporizador (evitando closures obsoletos):
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(); // limpieza al desmontar
}, []);
return (
<div>
<span>{elapsed}s</span>
<button onClick={start}>Iniciar</button>
<button onClick={stop}>Detener</button>
</div>
);
}Reenvío de refs en React 19 (sin necesidad de forwardRef):
interface FancyInputProps {
label: string;
ref?: React.Ref<HTMLInputElement>;
}
function FancyInput({ label, ref, ...props }: FancyInputProps) {
return (
<label>
{label}
<input ref={ref} {...props} />
</label>
);
}
// Padre
function Form() {
const inputRef = useRef<HTMLInputElement>(null);
return (
<>
<FancyInput label="Nombre" ref={inputRef} />
<button onClick={() => inputRef.current?.focus()}>Enfocar</button>
</>
);
}useImperativeHandle (exponer una API personalizada):
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>;
}
// Padre
function App() {
const modalRef = useRef<ModalHandle>(null);
return (
<>
<button onClick={() => modalRef.current?.open()}>Abrir</button>
<Modal ref={modalRef}>
<p>¡Hola!</p>
<button onClick={() => modalRef.current?.close()}>Cerrar</button>
</Modal>
</>
);
}// Ref al DOM - pasa null como valor inicial, tipa el elemento
const divRef = useRef<HTMLDivElement>(null);
const canvasRef = useRef<HTMLCanvasElement>(null);
// Ref mutable - valor inicial no nulo
const countRef = useRef<number>(0); // MutableRefObject<number>
// Aceptar una prop ref en React 19
interface Props {
ref?: React.Ref<HTMLInputElement>;
}
// Tipar useImperativeHandle
interface Handle {
scrollToTop: () => void;
}
useImperativeHandle(ref, (): Handle => ({
scrollToTop: () => window.scrollTo(0, 0),
}));Leer un ref durante el render - inputRef.current es null durante el primer render porque el nodo del DOM aún no existe. Solución: Accede a los refs en manejadores de eventos, useEffect o tras una comprobación de null.
Los cambios en un ref no provocan re-renders - Actualizar ref.current no hará que el componente se vuelva a renderizar ni que se reflejen los cambios en el JSX. Solución: Si necesitas que la UI se actualice, usa useState en su lugar. Usa los refs solo para valores de los que la salida del render no dependa.
forwardRef está obsoleto en React 19 - React.forwardRef sigue funcionando pero ya no es necesario. Solución: Acepta ref como una prop normal: function MyComp({ ref }: { ref?: React.Ref<HTMLElement> }).
Asignar un ref a un elemento condicional - Si el elemento con ref se renderiza de forma condicional, .current será null cuando el elemento esté oculto. Solución: Comprueba siempre si es null antes de usarlo: ref.current?.focus().
El ref callback se dispara dos veces en StrictMode - En desarrollo con StrictMode, los ref callbacks se disparan con null y luego con el nodo, simulando montaje/desmontaje/re-montaje. Solución: Esto es lo esperado - asegúrate de que tu ref callback maneje null correctamente. En React 19, devuelve una función de limpieza en lugar de comprobar si es null.
| Alternativa | Úsala Cuando | No la Uses Cuando |
|---|---|---|
useState | El valor necesita provocar re-renders cuando cambia | Estás almacenando un ID de temporizador, un valor previo o un nodo del DOM |
document.getElementById | Prototipo rápido fuera de React | Código React de producción (rompe la encapsulación del componente) |
| Atributos de datos + CSS | Necesitas alternar estilos según el estado | Necesitas acceso programático al DOM (enfocar, desplazar, medir) |
useImperativeHandle | Quieres exponer una API limitada desde un componente hijo | Solo necesitas el nodo crudo del DOM |
useRef devuelve un objeto mutable { current: value } que persiste entre renders sin provocar re-renders. Úsalo para acceder al DOM (enfocar, desplazar, medir), almacenar IDs de temporizadores y mantener valores mutables de los que la UI no depende.
useState provoca un nuevo render cuando se actualiza - úsalo para valores que la UI muestrauseRef no provoca re-renders - úsalo para valores como IDs de temporizadores, valores previos o nodos del DOMLos refs del DOM se poblan después de que React monte el elemento. Durante el render, el nodo del DOM aún no existe. Accede a los refs en manejadores de eventos, useEffect o tras una comprobación de null: ref.current?.focus().
No. En React 19, ref es una prop normal. Acéptala directamente:
function Input({ ref, ...props }: { ref?: React.Ref<HTMLInputElement> }) {
return <input ref={ref} {...props} />;
}Un ref callback es una función que se pasa a la prop ref. React la llama con el nodo del DOM en el montaje y con null en el desmontaje. En React 19, puede devolver una función de limpieza. Úsalo cuando necesites lógica de configuración/desmontaje vinculada al nodo del DOM (como ResizeObserver).
const timerRef = useRef<ReturnType<typeof setInterval> | null>(null);
timerRef.current = setInterval(() => { ... }, 1000);
// Más tarde:
clearInterval(timerRef.current!);Esto evita problemas de closures obsoletos, ya que ref.current siempre apunta al valor más reciente.
Te permite personalizar el valor expuesto cuando un padre usa un ref sobre tu componente. En lugar de exponer el nodo crudo del DOM, expones una API limitada como { open(), close() }. Úsalo con moderación - prefiere las props para la mayoría de la comunicación.
Sí. Incrementa ref.current en el cuerpo del componente:
const renderCount = useRef(0);
renderCount.current += 1;Como los cambios en un ref no provocan re-renders, esto no causará un bucle infinito.
El StrictMode de React simula el desmontaje/re-montaje en desarrollo, así que el ref callback se dispara con null y luego con el nodo. Esto es lo esperado. En React 19, devuelve una función de limpieza desde el ref callback en lugar de comprobar si es null.
function usePrevious<T>(value: T): T | undefined {
const ref = useRef<T | undefined>(undefined);
useEffect(() => { ref.current = value; }, [value]);
return ref.current;
}El ref almacena el valor antiguo porque useEffect se ejecuta después del render.
Revisado por Chris St. John·Última actualización: 16 jul 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥