Hook useRef: referencias y valores que persisten entre renders
En esta clase continuamos justo donde lo dejamos con el buscador y el debounce. Ya tenemos búsqueda, paginación y filtros funcionando, y además aplicamos debounce para no llamar a la API en cada tecla. Pero tenemos un pequeño apaño que hay que corregir: estamos usando una variable global para guardar el timeoutId del debounce. Ha llegado el momento de usar useRef para hacerlo bien.
Contexto: cómo estaba el proyecto
Recordatorio rápido de lo que ya teníamos:
- Home con un buscador de ofertas
- Navegación a la página de resultados
- Paginación funcionando
- Filtros por tecnología, ubicación, experiencia
- Un buscador por texto con debounce para evitar hacer una petición por cada letra
En el código del custom hook del buscador, para implementar el debounce, hicimos algo como esto:
let timeoutId: number | null = null // 👈 variable fuera del hook
export function useSearchForm(/* props */) {
const handleTextChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const text = event.target.value
// cancelar timeout anterior
if (timeoutId) {
clearTimeout(timeoutId)
}
// crear nuevo timeout
timeoutId = window.setTimeout(() => {
onTextFilter(text)
}, 500)
}
// ...
}
Esto funcionaba, pero era un apaño temporal. Vamos a ver por qué.
Por qué una variable global es una mala idea
Problema 1 - Se comparte entre instancias
Si ese custom hook o componente se renderizase varias veces en la misma página:
- Todas las instancias compartirían el mismo
timeoutId - Un buscador podría cancelar el timeout de otro
- El comportamiento sería impredecible
Aunque en este proyecto solo tenemos un buscador, no deja de ser mala práctica.
Problema 2 - Dentro del hook tampoco vale
La alternativa ingenua sería:
export function useSearchForm(/* props */) {
let timeoutId: number | null = null // ❌ variable local
const handleTextChange = (event: React.ChangeEvent<HTMLInputElement>) => {
clearTimeout(timeoutId)
timeoutId = window.setTimeout(() => {
onTextFilter(event.target.value)
}, 500)
}
// ...
}
¿Por qué esto no funciona?
- React vuelve a ejecutar el hook en cada render
- Cada vez que se renderiza,
timeoutIdse inicializa otra vez anull - Pierdes la referencia al timeout anterior
clearTimeout(timeoutId)acaba siendo casi siempreclearTimeout(null), que no limpia nada
Necesitamos algo que:
- Guarde un valor entre renders
- No se reinicie al renderizar
- No dispare un nuevo render cada vez que lo actualizamos
Eso es exactamente lo que hace useRef.
Qué es useRef (y en qué se diferencia de useState)
useRef es un hook que te permite:
- Guardar un valor que persiste entre renderizados
- Leer y escribir ese valor
- Sin provocar re-renders cuando cambia
Comparación rápida con useState:
useState
- Guarda un valor
- Cuando lo actualizas, React vuelve a renderizar el componente
useRef
- Guarda un valor en la propiedad
.current - Cuando cambias
.current, React no re-renderiza
Piensa en useRef como:
Una caja donde guardas un valor que quieres recordar entre renders, pero que no afecta directamente a la UI.
Sintaxis básica de useRef
Primero, lo importamos:
import { useRef } from 'react'
Luego lo usamos así:
const myRef = useRef('hola')
console.log(myRef)
Si miras el console log, verás algo como:
{
current: 'hola'
}
Es decir:
useRef(valorInicial)devuelve un objeto- Ese objeto tiene una propiedad
.current - Dentro de
.currentvive el valor
Para leer o escribir el valor:
console.log(myRef.current) // 👉 'hola'
myRef.current = 'adiós'
console.log(myRef.current) // 👉 'adiós'
No hay setters, no hay setAlgo. Es mutación directa sobre .current.
Ejemplo práctico: contar cuántas veces se usa un input
Para entenderlo mejor, en el vídeo se hace un ejemplo: contar cuántas veces se ha escrito en el input del buscador.
Crear la referencia
const counterRef = useRef(0)
Usarla en handleTextChange
const handleTextChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const text = event.target.value
// actualizar estado controlado del input
setSearchText(text)
// incrementar contador
counterRef.current += 1
console.log('Usado el input tantas veces:', counterRef.current)
}
¿Qué pasa aquí?
Cada vez que el usuario escribe:
- Se renderiza el componente (porque cambiamos el estado
searchText) - Pero
counterRef.currentno se resetea - El valor se mantiene entre renders
- Cuando lo modificamos, no provocamos re-render por ello
Este ejemplo sirve para ver dos ideas clave:
- El valor persiste entre renderizados
- Cambiar
.currentno dispara un nuevo render
Usando useRef para arreglar el debounce
Volvemos al problema original: el timeoutId del debounce.
Antes: variable global
let timeoutId: number | null = null
export function useSearchForm(/* props */) {
const handleTextChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const text = event.target.value
setSearchText(text)
if (timeoutId) {
clearTimeout(timeoutId)
}
timeoutId = window.setTimeout(() => {
onTextFilter(text)
}, 500)
}
// ...
}
Después: ref dentro del hook
Vamos a mover ese valor a una referencia:
import { useRef, useState } from 'react'
export function useSearchForm({ onTextFilter, /* resto de props */ }) {
const [searchText, setSearchText] = useState('')
const timeoutId = useRef<number | null>(null) // 👈 ref para el timeout
const handleTextChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const text = event.target.value
// 1. Actualizamos el estado del input
setSearchText(text)
// 2. Si ya hay un timeout programado, lo cancelamos
if (timeoutId.current) {
clearTimeout(timeoutId.current)
}
// 3. Programamos un nuevo timeout y guardamos el id en la ref
timeoutId.current = window.setTimeout(() => {
onTextFilter(text)
}, 500)
}
// ...
return {
searchText,
handleTextChange,
// handleSubmit, etc...
}
}
Qué hemos ganado
timeoutId ahora:
- Vive dentro del hook
- Es independiente para cada instancia del hook
- Persiste entre renders
- Ya no necesitamos variables globales
- El código es más React friendly y escalable
Segundo uso clave: acceder al DOM con useRef
useRef no solo sirve para valores “normales”. También es la forma recomendada en React de acceder a elementos del DOM sin usar document.getElementById, querySelector, etc.
El código “old school” (mala práctica)
En el vídeo se enseña un caso real: un botón para limpiar el input de búsqueda.
Versión inicial:
const handleClearInput = (event: React.MouseEvent<HTMLButtonElement>) => {
event.preventDefault()
const input = document.getElementById('search-input') as HTMLInputElement | null
if (!input) return
input.value = ''
onTextFilter('') // para resetear los resultados
}
Problemas:
- Rompe el modelo de React, que quiere ser quien controle el DOM
- A largo plazo es difícil de mantener y razonar
- Es una mala práctica en componentes React modernos
Versión correcta con useRef
Paso 1 - Creamos una ref para el input:
const inputRef = (useRef < HTMLInputElement) | (null > null)
Paso 2 - Se la pasamos al input:
<input
ref={inputRef}
type="text"
name={idText}
value={searchText}
onChange={handleTextChange}
placeholder="Buscar empleos..."
/>
Paso 3 - Usamos la ref en el handler:
const handleClearInput = (event: React.MouseEvent<HTMLButtonElement>) => {
event.preventDefault()
if (!inputRef.current) return
// limpiar el input en la UI controlada
inputRef.current.value = ''
onTextFilter('') // o setSearchText('') si lo controlas por estado
}
Y la ref la puedes usar para hacer cosas como:
inputRef.current?.focus()después de limpiar- Mover el foco a ese input cuando entras en la página
- Hacer scroll hacia ese elemento
Por qué esto sí es una buena práctica
- No dependes del id ni del DOM global
- La referencia vive dentro del componente
- React sabe qué elemento está asociado a esa ref
- Sigues trabajando dentro del modelo declarativo de React
Resumen de casos de uso de useRef que has visto
En esta clase has visto dos usos muy importantes de useRef:
Guardar valores mutables que persisten entre renders
- Ejemplo:
timeoutId.currentpara el debounce - Ejemplo:
counterRef.currentpara contar cuántas veces se ha usado el input - Cambiar el valor no dispara un re-render
Guardar referencias a elementos del DOM
- Ejemplo:
inputRef.currentpara limpiar o enfocar el input - Evitamos
document.getElementByIdy similares - Usamos la forma “oficial” de React para acceder al DOM
Errores comunes con useRef que debes evitar
Olvidar .current
Es muy típico intentar hacer timeoutId = setTimeout(...). Con useRef siempre es timeoutId.current = ....
Usar useRef como sustituto de todo el estado
- Si el valor tiene que pintar algo en pantalla cuando cambia, entonces necesitas
useState. - Si es solo un valor de trabajo interno (timeout, contador, referencia, etc.),
useRefes perfecto.
Pensar que useRef dispara renders
Cambiar myRef.current no actualizará la UI por sí solo. Si quieres que algo se vea en pantalla, tiene que pasar por useState o venir de props.
Resumen final de la clase
En esta clase has aprendido:
- Por qué una variable global para el
timeoutIddel debounce es una mala práctica - Por qué una variable local dentro del hook tampoco sirve (se reinicia en cada render)
- Qué es
useRefy en qué se diferencia deuseState - Cómo funciona la estructura
{ current: valor } - Cómo contar cuántas veces se ha usado un input con una referencia
- Cómo migrar el debounce para usar
useRefen lugar de una variable global - Cómo acceder a elementos del DOM con
useRefen lugar dedocument.getElementById - Cuándo usar
useRefy cuándo seguir usandouseState
Con esto, tu buscador:
- Sigue teniendo debounce
- Ya no depende de variables globales
- Utiliza el patrón correcto de React para manejar valores que persisten entre renders y referencias al DOM