Skip to content

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:

  1. Acceder a un elemento del DOM (enfocar un input, medir un elemento, reproducir un video).
  2. 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.time o 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 memo o 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:

  1. Pasas la función a un componente envuelto en memo (si no, el hijo se re-renderiza igual).
  2. La función es dependencia de un useEffect o 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 ​

  1. 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).
  2. Crea un componente que reciba una lista de 5,000 números aleatorios y calcule con useMemo el 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, ref se 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.

Academia Web — aprende, copia y construye.