Refs
Accede a nodos del DOM, almacena valores mutables que persisten entre renders y expón handles desde componentes hijos.
Receta
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.
Ejemplos Básicos
Enfocar un input al hacer clic en un botón
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 (?.).
Desplazar un elemento a la vista
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.
Almacenar un valor sin volver a renderizar
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é.
Leer el valor de un input sin estado controlado
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".
Reproducir / pausar un vídeo
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.
Ejemplo Práctico
"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:
useRefpara obtener un handle sobre un elemento<input>para enfocarlo de forma programática- Limpiar el input y volver a enfocarlo con
inputRef.current?.focus() - Ref callback de React 19 con función de limpieza para
ResizeObserver - Refs para medir el DOM sin provocar re-renders innecesarios
Análisis Profundo
Cómo Funciona
useRef(initialValue)devuelve un objeto mutable{ current: initialValue }que persiste durante toda la vida del componente- Cuando se pasa a la prop
refde un elemento JSX, React establece.currenten el nodo del DOM tras el montaje y lo devuelve anullen el desmontaje - Cambiar
.currentno provoca un nuevo render — esta es la diferencia clave conuseState - En React 19, los ref callbacks pueden devolver una función de limpieza (como
useEffect), que se ejecuta cuando el elemento se desmonta o el ref cambia - En React 19,
refes una prop normal — no se necesita el envoltorioforwardRef
Parámetros y Valores de Retorno
useRef:
| 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 |
Comparación de Tipos de Ref
| 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) |
Variaciones
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>
</>
);
}Notas de TypeScript
// 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),
}));Trampas
-
Leer un ref durante el render —
inputRef.currentesnulldurante el primer render porque el nodo del DOM aún no existe. Solución: Accede a los refs en manejadores de eventos,useEffecto tras una comprobación de null. -
Los cambios en un ref no provocan re-renders — Actualizar
ref.currentno 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, usauseStateen su lugar. Usa los refs solo para valores de los que la salida del render no dependa. -
forwardRef está obsoleto en React 19 —
React.forwardRefsigue funcionando pero ya no es necesario. Solución: Aceptarefcomo una prop normal:function MyComp({ ref }: { ref?: React.Ref<HTMLElement> }). -
Asignar un ref a un elemento condicional — Si el elemento con
refse renderiza de forma condicional,.currentseránullcuando 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
nully luego con el nodo, simulando montaje/desmontaje/re-montaje. Solución: Esto es lo esperado — asegúrate de que tu ref callback manejenullcorrectamente. En React 19, devuelve una función de limpieza en lugar de comprobar si esnull.
Alternativas
| 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 |
Preguntas Frecuentes
¿Qué es useRef y cuándo debería usarlo?
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.
¿Cuál es la diferencia entre useRef y useState?
useStateprovoca un nuevo render cuando se actualiza — úsalo para valores que la UI muestrauseRefno provoca re-renders — úsalo para valores como IDs de temporizadores, valores previos o nodos del DOM
¿Por qué ref.current es null en el primer render?
Los 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().
¿Sigo necesitando forwardRef en React 19?
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} />;
}¿Qué es un ref callback y en qué se diferencia de useRef?
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).
¿Cómo almaceno un ID de temporizador con useRef?
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.
¿Qué es useImperativeHandle?
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.
¿Puedo usar useRef para rastrear cuántas veces se ha renderizado un componente?
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.
¿Por qué mi ref callback se dispara dos veces en StrictMode?
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.
¿Cómo creo un hook personalizado usePrevious con useRef?
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.
Relacionado
- Formularios — usar refs para inputs de formularios no controlados
- Eventos — adjuntar escuchadores de eventos nativos a través de refs
- Componentes — componer componentes que aceptan refs
- useState — cuando necesitas reactividad en lugar de un ref