Tema
useRef, useMemo y useCallback
Tres hooks que resuelven problemas específicos: useRef para acceder a elementos del DOM y guardar valores que no provocan renders, y useMemo / useCallback para memorizar cálculos y funciones. Aprenderás cuándo usarlos y, igual de importante, cuándo no.
useRef
Concepto
useRef devuelve un objeto { current: valor } que:
- Persiste entre renders (como el estado).
- Al cambiarlo no provoca un nuevo render (a diferencia del estado).
Analogía: el estado es una pizarra que todos ven (cambiarla redibuja la pantalla). Una ref es una libreta personal en tu bolsillo: puedes anotar lo que quieras sin que nadie se entere.
Dos usos principales:
- Acceder a un elemento del DOM (enfocar un input, medir un elemento, reproducir un video).
- Guardar valores mutables que no se muestran (id de un intervalo, valor anterior, contador de renders).
Acceder al DOM
jsx
import { useRef } from 'react'
export default function Busqueda() {
const inputRef = useRef(null)
return (
<>
{/* React asigna el elemento a inputRef.current */}
<input ref={inputRef} placeholder="Buscar..." />
<button onClick={() => inputRef.current.focus()}>Enfocar</button>
<button onClick={() => { inputRef.current.value = ''; inputRef.current.focus() }}>Limpiar</button>
</>
)
}jsx
// Reproductor de video
import { useRef, useState } from 'react'
export function Reproductor({ src }) {
const videoRef = useRef(null)
const [reproduciendo, setReproduciendo] = useState(false)
function alternar() {
if (reproduciendo) videoRef.current.pause()
else videoRef.current.play()
setReproduciendo(!reproduciendo)
}
return (
<div>
<video ref={videoRef} src={src} width="100%" onEnded={() => setReproduciendo(false)} />
<button onClick={alternar}>{reproduciendo ? '⏸ Pausar' : '▶ Reproducir'}</button>
</div>
)
}jsx
// Scroll automático al último mensaje de un chat
import { useEffect, useRef } from 'react'
export function Chat({ mensajes }) {
const finRef = useRef(null)
useEffect(() => {
finRef.current?.scrollIntoView({ behavior: 'smooth' })
}, [mensajes.length])
return (
<div style={{ height: 300, overflowY: 'auto' }}>
{mensajes.map((m) => <p key={m.id}>{m.texto}</p>)}
<div ref={finRef} />
</div>
)
}Guardar valores sin re-render
jsx
import { useRef, useState } from 'react'
export function Cronometro() {
const [ms, setMs] = useState(0)
const intervaloRef = useRef(null) // el id del intervalo no se muestra: va en una ref
function iniciar() {
if (intervaloRef.current) return
const inicio = Date.now() - ms
intervaloRef.current = setInterval(() => setMs(Date.now() - inicio), 10)
}
function detener() {
clearInterval(intervaloRef.current)
intervaloRef.current = null
}
return (
<div>
<h2>{(ms / 1000).toFixed(2)} s</h2>
<button onClick={iniciar}>Iniciar</button>
<button onClick={detener}>Detener</button>
<button onClick={() => { detener(); setMs(0) }}>Reiniciar</button>
</div>
)
}ref como prop (React 19)
En React 19 puedes pasar ref a tus propios componentes como cualquier otra prop (antes necesitabas forwardRef):
jsx
// Un input personalizado que expone su elemento DOM
function CampoTexto({ etiqueta, ref, ...props }) {
return (
<label>
{etiqueta}
<input ref={ref} {...props} />
</label>
)
}
function Formulario() {
const emailRef = useRef(null)
return (
<>
<CampoTexto etiqueta="Email" ref={emailRef} type="email" />
<button onClick={() => emailRef.current.focus()}>Ir al email</button>
</>
)
}Error común
Leer o escribir ref.current durante el render (en el cuerpo del componente). Las refs son para manejadores de eventos y efectos. Si un valor debe mostrarse en pantalla, usa estado.
useMemo
Concepto
useMemo memoriza el resultado de un cálculo y sólo lo recalcula cuando cambian sus dependencias.
jsx
const resultado = useMemo(() => calculoCostoso(a, b), [a, b])Analogía: si alguien te pregunta 10 veces "¿cuánto es 3.847 × 9.213?", haces la cuenta una vez, la anotas, y respondes de memoria hasta que cambien los números.
jsx
import { useMemo, useState } from 'react'
// Simula 10,000 productos
const PRODUCTOS = Array.from({ length: 10000 }, (_, i) => ({
id: i + 1,
nombre: `Producto ${i + 1}`,
precio: Math.round(Math.random() * 5000)
}))
export default function Catalogo() {
const [busqueda, setBusqueda] = useState('')
const [orden, setOrden] = useState('asc')
const [tema, setTema] = useState('claro') // estado que NO afecta a la lista
// Sólo se recalcula si cambia busqueda u orden (no al cambiar el tema)
const visibles = useMemo(() => {
console.log('Filtrando y ordenando…')
const texto = busqueda.toLowerCase()
return PRODUCTOS.filter((p) => p.nombre.toLowerCase().includes(texto)).toSorted((a, b) =>
orden === 'asc' ? a.precio - b.precio : b.precio - a.precio
)
}, [busqueda, orden])
return (
<div style={{ background: tema === 'oscuro' ? '#222' : '#fff', color: tema === 'oscuro' ? '#eee' : '#222' }}>
<button onClick={() => setTema((t) => (t === 'claro' ? 'oscuro' : 'claro'))}>Cambiar tema</button>
<input value={busqueda} onChange={(e) => setBusqueda(e.target.value)} placeholder="Buscar" />
<select value={orden} onChange={(e) => setOrden(e.target.value)}>
<option value="asc">Precio ↑</option>
<option value="desc">Precio ↓</option>
</select>
<p>{visibles.length} resultados</p>
<ul>{visibles.slice(0, 50).map((p) => <li key={p.id}>{p.nombre} — ${p.precio}</li>)}</ul>
</div>
)
}¿Cuándo vale la pena?
- Cálculos realmente costosos (filtrar/ordenar miles de elementos, transformaciones pesadas). Mídelo con
console.timeo el Profiler de React DevTools: si tarda más de ~1 ms, puede valer la pena. - Para mantener la misma referencia de un objeto o arreglo que pasas a un componente memorizado con
memoo que usas como dependencia de un efecto.
useCallback
Concepto
useCallback memoriza una función para que no se cree una nueva en cada render.
jsx
const manejar = useCallback(() => { /* ... */ }, [deps])
// es equivalente a:
const manejar = useMemo(() => () => { /* ... */ }, [deps])En JavaScript, cada render crea funciones nuevas: () => {} !== () => {}. Normalmente no importa. Importa cuando:
- Pasas la función a un componente envuelto en
memo(si no, el hijo se re-renderiza igual). - La función es dependencia de un
useEffecto de otro hook.
jsx
import { memo, useCallback, useState } from 'react'
// memo: el componente sólo se vuelve a renderizar si sus props cambian
const Fila = memo(function Fila({ tarea, onToggle, onEliminar }) {
console.log('Render fila', tarea.id)
return (
<li>
<input type="checkbox" checked={tarea.hecha} onChange={() => onToggle(tarea.id)} />
{tarea.texto}
<button onClick={() => onEliminar(tarea.id)}>✕</button>
</li>
)
})
export default function Tareas() {
const [tareas, setTareas] = useState([
{ id: 1, texto: 'Comprar café', hecha: false },
{ id: 2, texto: 'Llamar al cliente', hecha: false }
])
const [nuevo, setNuevo] = useState('')
// Sin useCallback, escribir en el input recrearía estas funciones
// y TODAS las filas se volverían a renderizar
const onToggle = useCallback((id) => {
setTareas((ts) => ts.map((t) => (t.id === id ? { ...t, hecha: !t.hecha } : t)))
}, [])
const onEliminar = useCallback((id) => {
setTareas((ts) => ts.filter((t) => t.id !== id))
}, [])
return (
<>
<input value={nuevo} onChange={(e) => setNuevo(e.target.value)} />
<button onClick={() => { setTareas([...tareas, { id: Date.now(), texto: nuevo, hecha: false }]); setNuevo('') }}>
Agregar
</button>
<ul>
{tareas.map((t) => <Fila key={t.id} tarea={t} onToggle={onToggle} onEliminar={onEliminar} />)}
</ul>
</>
)
}Observa que usamos la forma funcional setTareas((ts) => ...): así la función no depende de tareas y sus dependencias quedan vacías.
¿Debo memorizar todo?
No. Memorizar tiene un costo (memoria y comparar dependencias) y complica el código. Reglas prácticas:
| Situación | ¿Memorizar? |
|---|---|
| Cálculo simple (sumar un carrito de 10 productos) | ❌ |
| Filtrar/ordenar miles de elementos | ✅ useMemo |
Función pasada a un componente con memo | ✅ useCallback |
| Función usada como dependencia de un efecto | ✅ useCallback (o muévela dentro del efecto) |
| "Por si acaso" | ❌ |
React Compiler
El React Compiler (estable desde 2025) analiza tu código y aplica estas memorizaciones automáticamente. En proyectos que lo usan, casi nunca necesitarás escribir useMemo, useCallback ni memo a mano. Aun así, entender qué hacen te ayuda a leer código existente y a diagnosticar problemas de rendimiento. Lo verás en Rendimiento.
Ejercicios
- Crea un formulario con tres inputs donde, al presionar Enter en uno, el foco pase al siguiente (usa un arreglo de refs o tres refs).
- Crea un componente que reciba una lista de 5,000 números aleatorios y calcule con
useMemoel promedio, el mínimo y el máximo. Agrega un contador independiente y verifica en consola que el cálculo no se repite al usar el contador.
Ver solución
jsx
// 1
import { useRef } from 'react'
export function FormularioEnter() {
const refs = [useRef(null), useRef(null), useRef(null)]
function manejarTecla(e, indice) {
if (e.key !== 'Enter') return
e.preventDefault()
const siguiente = refs[indice + 1]
if (siguiente) siguiente.current.focus()
else alert('¡Formulario completo!')
}
return (
<form>
{['Nombre', 'Email', 'Teléfono'].map((campo, i) => (
<input key={campo} ref={refs[i]} placeholder={campo} onKeyDown={(e) => manejarTecla(e, i)} />
))}
</form>
)
}jsx
// 2
import { useMemo, useState } from 'react'
const NUMEROS = Array.from({ length: 5000 }, () => Math.round(Math.random() * 1000))
export function Estadisticas({ numeros = NUMEROS }) {
const [clics, setClics] = useState(0)
const stats = useMemo(() => {
console.log('Calculando estadísticas…')
const suma = numeros.reduce((a, b) => a + b, 0)
return {
promedio: (suma / numeros.length).toFixed(2),
min: Math.min(...numeros),
max: Math.max(...numeros)
}
}, [numeros])
return (
<div>
<p>Promedio: {stats.promedio} · Mín: {stats.min} · Máx: {stats.max}</p>
<button onClick={() => setClics((c) => c + 1)}>Clics: {clics}</button>
</div>
)
}Resumen
- useRef:
{ current }persistente que no provoca renders. Úsalo para el DOM y valores internos (ids de intervalos). - En React 19,
refse pasa como una prop normal a tus componentes. - useMemo memoriza resultados de cálculos costosos; useCallback memoriza funciones.
- Memoriza sólo cuando hay un problema real o con
memo; el React Compiler lo hace automáticamente.