La lección anterior dejó dos cabos sueltos. El primero es técnico: el temporizador de DisponibilidadEstacion devolvía un identificador que había que guardar en algún sitio para poder cancelarlo, y una variable normal no sirve porque se pierde en cada render, mientras que el estado provocaría un repintado inútil. El segundo viene de más atrás, de 03-05: allí se mencionó que se puede leer un campo de formulario accediendo directamente al nodo del DOM, y se aplazó la explicación. useRef resuelve las dos cosas, porque en realidad son la misma: es una caja que sobrevive a los renders sin formar parte de ellos. En esta lección verás sus dos usos —referencia a nodos del DOM y valor mutable de instancia—, cuándo se puede leer y cuándo no, cómo pasar referencias a tus propios componentes en React 19 (donde ref ya es una prop normal), las referencias de callback para elementos dinámicos, y la frontera exacta entre lo que merece useRef y lo que merece useState.
Contenido
- El problema: recordar sin repintar
useRef, la caja mutableuseState,useRefy variable normal: tabla comparativa- Uso 1: referencia a un nodo del DOM
- Cuatro casos reales en CicloUrbano
- Cuándo está disponible
ref.current - Uso 2: valor mutable de instancia
- La regla de oro: no leer ni escribir
currentdurante el render - Cerrando la deuda de 03-05:
reffrente aFormData - Pasar referencias a componentes propios en React 19
- Referencias de callback para elementos dinámicos
- Cuándo NO usar
useRef
- El problema: recordar sin repintar
Imagina un cronómetro en el panel de operario de CicloUrbano: arranca al pulsar «Iniciar revisión» y se para al pulsar «Finalizar». Necesitas guardar el identificador que devuelve setInterval para poder cancelarlo. Veamos las dos opciones que ya conoces y por qué ninguna vale.
// ❌ Opción A: variable normal. Se pierde en cada render.
function Cronometro() {
const [segundos, setSegundos] = useState(0);
let identificador = null; // nace y muere con cada render
function manejarIniciar() {
identificador = setInterval(() => setSegundos((p) => p + 1), 1000);
}
function manejarParar() {
clearInterval(identificador); // aquí ya vale null: no para nada
}
}El primer setSegundos provoca un render, la función del componente se ejecuta otra vez y identificador vuelve a valer null. El cronómetro no se puede parar.
// ❌ Opción B: estado. Funciona, pero repinta sin necesidad.
const [identificador, setIdentificador] = useState(null);Ahora sí sobrevive, pero cada setIdentificador provoca un render completo del componente para guardar un número que no aparece en ninguna parte de la pantalla. Es trabajo tirado, y en componentes grandes se nota.
Lo que necesitamos es una tercera cosa: algo que persista como el estado pero que no dispare renders como una variable normal. Eso es exactamente useRef.
useRef, la caja mutable
useRef, la caja mutableuseRef devuelve siempre el mismo objeto, render tras render, con una única propiedad:
Y ahí acaba la magia. No hay nada más. Tres propiedades que definen su comportamiento:
- El objeto devuelto es estable. React nunca lo sustituye; en el render 1 y en el render 500 es la misma referencia. Por eso no hace falta ponerlo en las dependencias de un efecto (05-02).
currentes mutable y lo cambias tú. No hay actualizador: se asigna directamente,referencia.current = algo. React no interviene.- Cambiar
currentno provoca ningún render. React ni se entera.
El cronómetro, resuelto:
// src/componentes/CronometroRevision.jsx
import { useState, useRef } from 'react';
function CronometroRevision({ bicicleta }) {
const [segundos, setSegundos] = useState(0);
const identificadorIntervalo = useRef(null); // ← la caja que sobrevive
function manejarIniciar() {
if (identificadorIntervalo.current !== null) return; // ya está en marcha
identificadorIntervalo.current = setInterval(() => {
setSegundos((previos) => previos + 1);
}, 1000);
}
function manejarParar() {
clearInterval(identificadorIntervalo.current);
identificadorIntervalo.current = null;
}
return (
<>
<p>Revisión de {bicicleta.modelo}: {segundos} s</p>
<button type="button" onClick={manejarIniciar}>Iniciar revisión</button>
<button type="button" onClick={manejarParar}>Finalizar</button>
</>
);
}
export default CronometroRevision;Fíjate en el reparto: segundos es estado porque se ve en pantalla y debe repintar; identificadorIntervalo es referencia porque es fontanería interna que nadie mira.
useState, useRef y variable normal: tabla comparativa
useState, useRef y variable normal: tabla comparativaVariable normal (let x) |
useRef |
useState |
|
|---|---|---|---|
| ¿Persiste entre renders? | ❌ No, se reinicia | ✅ Sí | ✅ Sí |
| ¿Al cambiarla se repinta? | ❌ No | ❌ No | ✅ Sí |
| Cómo se cambia | x = valor |
ref.current = valor |
setX(valor) |
| ¿Se puede leer durante el render? | ✅ Sí | ⚠️ No se debe | ✅ Sí |
| ¿Se puede escribir durante el render? | ✅ Sí | ❌ No | ❌ No |
| ¿Va en las dependencias de un efecto? | ✅ Sí (es reactiva) | ❌ No (es estable) | ✅ Sí |
| Para qué sirve | Cálculos locales de un render | Recordar sin repintar; acceder al DOM | Datos que se ven en pantalla |
La frontera es una sola pregunta: ¿este dato aparece en lo que se dibuja? Si la respuesta es sí, es estado. Si es no, probablemente sea una referencia.
- Uso 1: referencia a un nodo del DOM
Este es el uso más visible. Se declara la referencia, se pasa como atributo ref de un elemento de JSX, y React coloca ahí el nodo real del navegador.
import { useRef } from 'react';
function EjemploFoco() {
const campo = useRef(null); // 1. arranca a null
function manejarEnfocar() {
campo.current.focus(); // 3. campo.current ES el <input> real
}
return (
<>
<input ref={campo} type="text" /> {/* 2. React pone el nodo en current */}
<button type="button" onClick={manejarEnfocar}>Ir al campo</button>
</>
);
}El flujo, con precisión:
flowchart TD
A["Render: useRef(null) devuelve { current: null }"] --> B["JSX declara ref={campo}"]
B --> C["Commit: React crea/actualiza el nodo del DOM"]
C --> D["React asigna campo.current = nodo real"]
D --> E["Efectos y manejadores ya pueden usar campo.current"]
E --> F{"¿El elemento se desmonta?"}
F -- "Sí" --> G["React asigna campo.current = null"]
style D fill:#dcfce7
style G fill:#fecaca
Es importante entender que React se encarga de rellenar y vaciar current. Tú no lo asignas nunca en este uso: solo lo lees.
- Cuatro casos reales en CicloUrbano
Enfocar el primer campo del formulario de reserva
Cuando el usuario pulsa «Reservar» en una tarjeta, el formulario aparece y lo cortés es dejar el cursor listo. Además es un requisito de accesibilidad (03-06): quien navega con teclado no debería tener que tabular hasta allí.
// src/componentes/FormularioReserva.jsx (fragmento)
import { useRef, useEffect } from 'react';
function FormularioReserva({ alCrearReserva, usuarioId = 'usr-01' }) {
const campoBicicleta = useRef(null);
useEffect(() => {
campoBicicleta.current?.focus(); // al montarse, el foco va al primer campo
}, []);
return (
<form noValidate onSubmit={manejarEnvio}>
<label htmlFor="bicicletaId">Bicicleta</label>
<select id="bicicletaId" name="bicicletaId" ref={campoBicicleta} …>
…
</select>
…
</form>
);
}El ?. no es decorativo: si el componente se renderiza con el formulario oculto, current seguirá siendo null y el acceso directo lanzaría un error.
Desplazar la lista hasta la bicicleta seleccionada
// src/componentes/ListaBicicletas.jsx (fragmento)
function ListaBicicletas({ bicicletas = [], estaciones = [], idSeleccionada, alSeleccionar, alReservar }) {
const tarjetaSeleccionada = useRef(null);
useEffect(() => {
tarjetaSeleccionada.current?.scrollIntoView({ behavior: 'smooth', block: 'center' });
}, [idSeleccionada]);
return (
<ul className={estilos.lista}>
{bicicletas.map((bicicleta) => (
<li
key={bicicleta.id}
ref={bicicleta.id === idSeleccionada ? tarjetaSeleccionada : null}
>
<TarjetaBicicleta … />
</li>
))}
</ul>
);
}El truco es pasar la referencia solo al elemento que interesa y null a los demás. React asigna current únicamente en ese, y el efecto se dispara cuando cambia idSeleccionada.
Medir un elemento
function ResumenFlota({ flota }) {
const contenedor = useRef(null);
const [caben, setCaben] = useState(0);
useEffect(() => {
const ancho = contenedor.current.getBoundingClientRect().width;
setCaben(Math.floor(ancho / 260)); // 260 px por tarjeta
}, []);
return <div ref={contenedor}>…</div>;
}Medir es una operación que solo se puede hacer sobre el DOM real: React no conoce los píxeles. Por eso vive en un efecto, después del commit.
Controlar un <dialog> nativo
Modal (04-02) usaba un <div> con role="dialog". El elemento nativo <dialog> ofrece gratis el foco atrapado y el cierre con Escape, pero se abre y se cierra con métodos imperativos, no con atributos:
// src/componentes/Modal.jsx (variante con <dialog> nativo)
import { useRef, useEffect } from 'react';
import estilos from './Modal.module.css';
/**
* Props:
* - titulo (cadena, obligatorio)
* - abierto (booleano, obligatorio)
* - children (contenido)
* - alCerrar (función, obligatorio)
*/
function Modal({ titulo, abierto, children, alCerrar }) {
const dialogo = useRef(null);
useEffect(() => {
const nodo = dialogo.current;
if (!nodo) return;
if (abierto && !nodo.open) {
nodo.showModal(); // método imperativo: no hay atributo equivalente
} else if (!abierto && nodo.open) {
nodo.close();
}
}, [abierto]);
return (
<dialog ref={dialogo} className={estilos.ventana} onClose={alCerrar}>
<h2>{titulo}</h2>
{children}
<button type="button" onClick={alCerrar}>Cerrar</button>
</dialog>
);
}
export default Modal;Este ejemplo resume la filosofía: React declara qué hay en pantalla; cuando el navegador solo ofrece una API imperativa, useRef es el puente.
- Cuándo está disponible
ref.current
ref.current| Momento | Valor de ref.current |
|---|---|
| Durante el primer render | null (o el valor inicial que pasaste) |
| Durante cualquier render posterior | El nodo del render anterior — no lo uses |
En un efecto (useEffect) |
✅ El nodo actual, ya en el DOM |
| En un manejador de eventos | ✅ El nodo actual |
| Después de desmontarse el elemento | null (React lo limpia) |
La consecuencia práctica: no puedes usar el nodo durante el render. Esto no funciona:
// ❌ current es null en el primer render: TypeError
function PanelMal() {
const caja = useRef(null);
const alto = caja.current.offsetHeight; // 💥
return <div ref={caja}>…</div>;
}Y aunque no fallara, sería incorrecto: el render debe ser puro (04-03) y leer el DOM lo hace depender de un estado externo. El sitio para eso es un efecto.
- Uso 2: valor mutable de instancia
Además de nodos, current puede guardar cualquier cosa. Tres patrones que aparecen a diario.
Guardar el identificador de un temporizador
Ya lo has visto en CronometroRevision. Es el caso canónico: un dato de fontanería que debe sobrevivir y que nadie mira.
Guardar el valor previo de una prop o de un estado
Útil para animar un cambio, registrar una transición o comparar.
// src/componentes/ContadorPlazas.jsx (fragmento)
import { useState, useRef, useEffect } from 'react';
function ContadorPlazas({ plazasLibres, estacion }) {
const plazasPrevias = useRef(plazasLibres);
const [tendencia, setTendencia] = useState('estable');
useEffect(() => {
if (plazasLibres > plazasPrevias.current) setTendencia('subiendo');
else if (plazasLibres < plazasPrevias.current) setTendencia('bajando');
else setTendencia('estable');
plazasPrevias.current = plazasLibres; // se escribe DENTRO del efecto
}, [plazasLibres]);
return (
<p>
{estacion.nombre}: {plazasLibres} plazas ({tendencia})
</p>
);
}Lo importante es dónde se escribe current: dentro del efecto, es decir, después del render. Si se escribiera en el cuerpo del componente, el valor «previo» ya se habría machacado antes de poder compararlo.
Contar renders en depuración
function TarjetaBicicleta({ bicicleta, nombreEstacion, alSeleccionar, alReservar }) {
const renders = useRef(0);
useEffect(() => {
renders.current += 1;
console.log(`TarjetaBicicleta ${bicicleta.id}: render nº ${renders.current}`);
});
…
}Con useState este contador sería un bucle infinito: incrementarlo provoca un render, que lo incrementa otra vez. Con useRef no pasa nada, porque no dispara ningún render. Es una herramienta habitual para investigar problemas de rendimiento antes de abrir el Profiler (08-05).
- La regla de oro: no leer ni escribir
current durante el render
current durante el renderDurante el render, un componente no debe leer ni escribir
ref.current. Solo en efectos y manejadores de eventos.
Escribir durante el render rompe la pureza: dos llamadas a la misma función con los mismos argumentos dejarían de producir el mismo resultado, y React se reserva el derecho de llamar a los componentes más de una vez (StrictMode) o de abandonar un render a medias.
// ❌ escritura durante el render: impuro, y con StrictMode cuenta el doble
function Mal() {
const visitas = useRef(0);
visitas.current += 1;
return <p>{visitas.current}</p>;
}Leer durante el render es menos grave pero igual de traicionero: como cambiar current no repinta, lo que pintes con ese valor puede quedarse desactualizado en pantalla sin que nada lo corrija. Si un dato tiene que verse, es estado.
La excepción tolerada es la inicialización perezosa de la referencia, cuando el valor inicial es caro de construir:
function ReproductorMapa() {
const instancia = useRef(null);
if (instancia.current === null) {
instancia.current = crearMapa(); // se ejecuta una sola vez; nunca lee un valor cambiante
}
…
}Se acepta porque la asignación ocurre exactamente una vez y el resultado no depende del render.
- Cerrando la deuda de 03-05:
ref frente a FormData
ref frente a FormDataEn 03-05 quedó abierta la comparación entre las dos formas de leer un campo no controlado. Aquí están las dos, lado a lado, sobre el mismo formulario de incidencias de CicloUrbano.
// Con FormData: lee TODOS los campos a partir de su atributo name
function FormularioIncidencia({ alRegistrar }) {
function manejarEnvio(evento) {
evento.preventDefault();
const datos = new FormData(evento.target);
alRegistrar({
bicicletaId: datos.get('bicicletaId'),
descripcion: datos.get('descripcion'),
urgente: datos.get('urgente') === 'on'
});
evento.target.reset();
}
…
}
// Con useRef: una referencia por campo
function FormularioIncidencia({ alRegistrar }) {
const campoBicicleta = useRef(null);
const campoDescripcion = useRef(null);
const campoUrgente = useRef(null);
function manejarEnvio(evento) {
evento.preventDefault();
alRegistrar({
bicicletaId: campoBicicleta.current.value,
descripcion: campoDescripcion.current.value,
urgente: campoUrgente.current.checked
});
campoBicicleta.current.focus(); // ← esto FormData no lo puede hacer
}
…
}| Criterio | FormData |
useRef |
|---|---|---|
| Leer todos los campos al enviar | ✅ Una línea, sin declarar nada | ❌ Una referencia por campo |
| Añadir un campo nuevo | ✅ Basta con ponerle name |
❌ Hay que declarar otra referencia |
| Leer un único campo suelto | Válido | Válido |
Actuar sobre el nodo: foco, select(), scrollIntoView |
❌ Imposible | ✅ Es su terreno |
| Integrar una biblioteca que necesita el nodo | ❌ Imposible | ✅ Es su terreno |
| Valores de tipo fichero | Válido (datos.get('foto')) |
Válido (campo.current.files) |
Conclusión: FormData para leer, useRef para actuar. Y son perfectamente combinables: leer con FormData en el submit y mantener una sola referencia al primer campo para devolverle el foco tras enviar es el reparto más limpio.
- Pasar referencias a componentes propios en React 19
Poner ref en un elemento del DOM funciona siempre. Ponerlo en un componente propio es otra historia, porque <TarjetaBicicleta /> no es un nodo: es una función.
En React 19 esto ya se resuelve solo: ref es una prop normal de los componentes de función. Basta con recibirla y reenviarla:
// src/componentes/CampoTexto.jsx (React 19)
function CampoTexto({ etiqueta, id, ref, ...resto }) {
return (
<p>
<label htmlFor={id}>{etiqueta}</label>
<input id={id} ref={ref} {...resto} />
</p>
);
}
export default CampoTexto;// Uso desde FormularioReserva
const campoFecha = useRef(null);
<CampoTexto ref={campoFecha} id="fechaInicio" etiqueta="Fecha de inicio" type="datetime-local" />
// campoFecha.current es el <input> de dentroEn React 18 y anteriores ref no llegaba como prop: React la interceptaba. Había que envolver el componente en forwardRef, y lo verás continuamente en código existente y en bibliotecas:
// Forma anterior, todavía funcional en React 19 (obsoleta pero soportada)
import { forwardRef } from 'react';
const CampoTexto = forwardRef(function CampoTexto({ etiqueta, id, ...resto }, ref) {
return (
<p>
<label htmlFor={id}>{etiqueta}</label>
<input id={id} ref={ref} {...resto} />
</p>
);
});| React 18 | React 19 | |
|---|---|---|
Recibir ref en un componente de función |
forwardRef(fn) |
ref llega como prop normal |
Código antiguo con forwardRef |
Obligatorio | Sigue funcionando, marcado como obsoleto |
Y una pieza más, mencionada para que la reconozcas: useImperativeHandle permite que un componente exponga a su padre un objeto propio en lugar del nodo del DOM (por ejemplo { enfocar, limpiar }), limitando lo que el padre puede tocar. Es una herramienta de bibliotecas de componentes, poco frecuente en código de aplicación.
- Referencias de callback para elementos dinámicos
Cuando el número de elementos no se conoce de antemano —una referencia por cada tarjeta de bicicleta— no puedes declarar un useRef por elemento: violaría la regla 1 de los hooks (04-04), que prohíbe llamarlos dentro de bucles.
La solución es pasar una función en el atributo ref. React la llama con el nodo al montarlo:
// src/componentes/ListaBicicletas.jsx (fragmento con referencias por elemento)
import { useRef } from 'react';
function ListaBicicletas({ bicicletas = [], idSeleccionada, ...resto }) {
const nodosPorId = useRef(new Map()); // UNA sola referencia, con un mapa dentro
function desplazarHasta(id) {
nodosPorId.current.get(id)?.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
return (
<ul>
{bicicletas.map((bicicleta) => (
<li
key={bicicleta.id}
ref={(nodo) => {
nodosPorId.current.set(bicicleta.id, nodo);
// React 19: la función puede devolver una LIMPIEZA
return () => nodosPorId.current.delete(bicicleta.id);
}}
>
<TarjetaBicicleta bicicleta={bicicleta} {...resto} />
</li>
))}
</ul>
);
}Dos cosas que cambian en React 19:
- La función de
refpuede devolver una función de limpieza, igual que un efecto. React la ejecuta al desmontar el elemento. Antes había que apañarlo comprobando si el argumento eranull. - Precisamente por eso, en React 19 la función de
refya no debe devolver otra cosa. Cuidado con la forma abreviadaref={(nodo) => mapa.set(id, nodo)}, que devuelve el mapa sin querer: hay que usar llaves.
// ❌ devuelve el Map: React lo interpretaría como función de limpieza
ref={(nodo) => nodosPorId.current.set(bicicleta.id, nodo)}
// ✅ con llaves: no devuelve nada, o devuelve una limpieza explícita
ref={(nodo) => { nodosPorId.current.set(bicicleta.id, nodo); }}
- Cuándo NO usar
useRef
useRefuseRef es tentador porque «funciona» y no repinta. Estos son los usos que hay que rechazar:
- Guardar datos que se muestran en pantalla. El síntoma es inconfundible: cambias
current, la pantalla no se entera, y acabas añadiendo unuseStatefalso (const [, forzar] = useState(0)) para forzar el repintado. Si tienes que forzar un render, ese dato era estado desde el principio. - Sustituir al estado «por rendimiento». El coste de un render bien planteado es despreciable; el coste de una interfaz que muestra datos viejos, no. La optimización tiene su propio módulo (Módulo 8) y sus propias herramientas.
- Manipular el DOM que React controla. Cambiar
nodo.textContent, añadir clases conclassListo borrar hijos a mano entra en conflicto con la reconciliación (01-05): React puede sobrescribir tus cambios en el siguiente render, o fallar al no encontrar lo que esperaba. Se puede tocar lo que React no gestiona —foco, scroll, selección, reproducción de un vídeo— pero no la estructura ni el contenido. - Como caché de cálculos. Para eso está
useMemo(08-03), que además invalida correctamente cuando cambian las entradas.
// ❌ Antipatrón: estado disfrazado de referencia
const filtro = useRef('todos');
function manejarCambioTipo(tipo) {
filtro.current = tipo; // la lista NO se actualiza; no hay render
}
// ✅ Se ve en pantalla → es estado
const [tipoElegido, setTipoElegido] = useState('todos');Como apunte final relacionado, React ofrece useId para generar identificadores únicos y estables con los que asociar label y campos sin colisiones cuando un formulario se repite en la misma página. No tiene que ver con el DOM directo, pero completa el kit de accesibilidad de 03-06:
const idFecha = useId();
<label htmlFor={idFecha}>Fecha de inicio</label>
<input id={idFecha} type="datetime-local" />Errores Comunes y Consejos
- Acceder a
ref.currentdurante el render. En el primer render valenully el acceso lanzaTypeError. Usa efectos o manejadores, y?.como red de seguridad. - Olvidar que React vacía
currental desmontar. Un temporizador que use la referencia después puede encontrarse unnull. - Escribir
currenten el cuerpo del componente. Rompe la pureza y conStrictModelos contadores salen dobles. - Usar
useRefpara datos visibles. Si acabas necesitando forzar un render, ese dato era estado. - Poner la referencia en las dependencias de un efecto. El objeto es estable: no aporta nada y confunde a quien lee.
- Devolver un valor sin querer en una
refde callback. En React 19 se interpreta como función de limpieza. Usa llaves. - Modificar el DOM que React gestiona. Toca solo lo que React no controla: foco, scroll, selección, media.
- Usar
forwardRefen código nuevo con React 19. Ya no hace falta; resérvalo para mantener código existente. - Consejo: al declarar una referencia, escribe en el nombre lo que guarda (
campoBicicleta,identificadorIntervalo,nodosPorId). Una referencia llamadarefno dice nada al que la lee. - Consejo: si dudas entre
useStateyuseRef, pregúntate si al cambiar ese valor la pantalla debería verse distinta. Si sí, estado; si no, referencia.
Ejercicios
Ejercicio 1. Este componente pretende contar cuántas veces ha cambiado la bicicleta seleccionada y mostrarlo en pantalla, pero no funciona. Explica por qué y corrígelo.
function ContadorSelecciones({ bicicletaSeleccionada }) {
const cambios = useRef(0);
useEffect(() => {
cambios.current += 1;
}, [bicicletaSeleccionada]);
return <p>Has cambiado de bicicleta {cambios.current} veces</p>;
}Ejercicio 2. Escribe BuscadorBicicletas de forma que, además de su comportamiento actual, ofrezca un botón «Limpiar» que vacíe el campo y devuelva el foco al campo de búsqueda. Explica por qué el foco necesita una referencia y el texto no.
Ejercicio 3. PanelReserva debe abrirse en un <dialog> nativo y cerrarse tanto con el botón «Cancelar» como con la tecla Escape (que el navegador maneja solo). Escribe el componente DialogoReserva que envuelve a PanelReserva, con props abierto, alCerrar y children, asegurándote de que el estado de React y el estado del <dialog> no se desincronizan.
Soluciones
Solución 1.
El fallo es de categoría: cambios se muestra en pantalla, así que debería ser estado. Al escribir en cambios.current no se produce ningún render, de modo que el <p> sigue mostrando el valor con el que se pintó por última vez. El contador sube por dentro y la pantalla se queda en 0 (o en el número que hubiera cuando otro cambio provocó un repintado por casualidad, que es peor: el error se vuelve intermitente).
function ContadorSelecciones({ bicicletaSeleccionada }) {
const [cambios, setCambios] = useState(0);
const esPrimerRender = useRef(true); // esto SÍ es una referencia legítima
useEffect(() => {
if (esPrimerRender.current) {
esPrimerRender.current = false; // el montaje inicial no cuenta como cambio
return;
}
setCambios((previos) => previos + 1);
}, [bicicletaSeleccionada]);
return <p>Has cambiado de bicicleta {cambios} veces</p>;
}La solución enseña las dos caras: cambios pasa a useState porque se ve; esPrimerRender se queda como useRef porque es fontanería interna que no debe repintar nada. Y setCambios usa la forma funcional (05-01) porque el nuevo valor depende del anterior.
Solución 2.
// src/componentes/BuscadorBicicletas.jsx
import { useState, useRef } from 'react';
/**
* Props:
* - alBuscar (función, obligatoria)
*/
function BuscadorBicicletas({ alBuscar }) {
const [texto, setTexto] = useState('');
const campo = useRef(null);
function manejarCambio(evento) {
setTexto(evento.target.value);
alBuscar(evento.target.value);
}
function manejarLimpiar() {
setTexto('');
alBuscar('');
campo.current?.focus(); // acción sobre el nodo: solo posible con la referencia
}
return (
<div>
<input
ref={campo}
type="search"
value={texto}
onChange={manejarCambio}
aria-label="Buscar bicicletas por modelo"
/>
<button type="button" onClick={manejarLimpiar} disabled={texto === ''}>
Limpiar
</button>
</div>
);
}
export default BuscadorBicicletas;Por qué el texto no necesita referencia y el foco sí: el texto se ve en el campo, así que es estado; React lo pinta con value={texto} y basta con ponerlo a '' para vaciar el campo. El foco, en cambio, no es un dato que React dibuje: es una propiedad del navegador. No existe ningún atributo de JSX que diga «este campo tiene el foco ahora»; solo existe el método focus() del nodo. Por eso hace falta llegar hasta el nodo real. Es la distinción exacta entre lo declarativo y lo imperativo.
Solución 3.
// src/componentes/DialogoReserva.jsx
import { useRef, useEffect } from 'react';
import estilos from './DialogoReserva.module.css';
/**
* Envoltorio de PanelReserva en un <dialog> nativo.
* Props:
* - abierto (booleano, obligatorio)
* - alCerrar (función, obligatorio)
* - children (contenido del diálogo)
*/
function DialogoReserva({ abierto, alCerrar, children }) {
const dialogo = useRef(null);
useEffect(() => {
const nodo = dialogo.current;
if (!nodo) return;
if (abierto && !nodo.open) {
nodo.showModal();
} else if (!abierto && nodo.open) {
nodo.close();
}
}, [abierto]);
return (
<dialog
ref={dialogo}
className={estilos.dialogo}
onClose={alCerrar} // ← se dispara también con Escape
onCancel={(evento) => { // Escape lanza 'cancel' antes de 'close'
evento.preventDefault(); // evitamos el cierre nativo directo…
alCerrar(); // …y dejamos que el estado de React lo ordene
}}
aria-labelledby="titulo-dialogo-reserva"
>
<h2 id="titulo-dialogo-reserva">Confirmar reserva</h2>
{children}
</dialog>
);
}
export default DialogoReserva;Las claves de la sincronización: las comprobaciones !nodo.open y nodo.open evitan llamar a showModal() sobre un diálogo ya abierto —lo que lanza un error del navegador— y a close() sobre uno cerrado. Y onClose es imprescindible porque el navegador puede cerrar el diálogo por su cuenta con Escape: sin ese aviso, el estado de React seguiría diciendo abierto: true mientras el diálogo ya está cerrado, y el siguiente intento de abrirlo no haría nada. Este es el problema típico al integrar un elemento con estado propio: hay que propagar sus cambios de vuelta a React, igual que un campo controlado propaga los suyos con onChange (03-04).
Conclusión
useRef es una caja { current } que sobrevive a los renders y que, al cambiar, no provoca ninguno. De ahí salen sus dos usos: guardar un valor mutable de instancia —el identificador de un temporizador, el valor previo de una prop, un contador de depuración— y sostener una referencia a un nodo del DOM para hacer lo que React no expresa de forma declarativa: enfocar un campo, medir un elemento, desplazar la lista hasta la bicicleta seleccionada o abrir un <dialog>. Has visto cuándo current está disponible (después del commit, nunca durante el render), la regla de no leerlo ni escribirlo mientras se renderiza, la comparación cerrada con FormData que quedó abierta en 03-05, cómo ref es ya una prop normal en React 19 —con forwardRef reducido a código heredado—, y las referencias de callback con limpieza para elementos que aparecen y desaparecen. Y sobre todo tienes la frontera clara: si el dato se ve, es estado; si no se ve, quizá sea una referencia.
Con useState, useEffect y useRef ya controlas lo que un componente recuerda y cómo se conecta con el exterior. Pero sigue habiendo una deuda pendiente desde 04-01: cuando un dato tiene que llegar desde App hasta un componente enterrado cinco niveles más abajo, hoy solo sabes hacerlo pasando props por cada escalón, aunque los componentes intermedios no las usen para nada. Esa perforación de props ensucia todas las firmas del camino y convierte cualquier cambio en un recorrido a mano por media aplicación. React tiene un canal directo entre un ancestro y cualquiera de sus descendientes, sin escalas. La próxima lección es Hook useContext.
Curso de React
Módulo 1: Introducción a React
- ¿Qué es React?
- Configuración del Entorno de Desarrollo
- Hola Mundo en React
- JSX: Extensión de Sintaxis de JavaScript
- Cómo Renderiza React: Virtual DOM y Reconciliación
Módulo 2: Componentes de React
- Entendiendo los Componentes
- Componentes Funcionales vs de Clase
- Props: Pasando Datos a Componentes
- State: Gestión del Estado del Componente
- Estilos en los Componentes: CSS, Módulos y Utilidades
Módulo 3: Trabajando con Eventos
- Manejo de Eventos en React
- Renderizado Condicional
- Listas y Claves
- Formularios y Componentes Controlados
- Validación de Formularios y Componentes No Controlados
- Accesibilidad en Componentes Interactivos
Módulo 4: Conceptos Avanzados de Componentes
- Elevando el Estado
- Composición vs Herencia
- Métodos del Ciclo de Vida de React
- Hooks: Introducción y Uso Básico
- Límites de Error: Capturar Fallos en la Interfaz
Módulo 5: Hooks de React
- Hook useState
- Hook useEffect
- Hook useRef y Acceso al DOM
- Hook useContext
- Hook useReducer
- Hooks Personalizados
Módulo 6: Enrutamiento en React
- Introducción a React Router
- Configuración de React Router
- Rutas Anidadas
- Navegación Programática
- Rutas Protegidas y Control de Acceso
Módulo 7: Gestión del Estado
- Introducción a la Gestión del Estado
- API de Contexto
- Redux: Introducción y Configuración
- Redux: Acciones y Reductores
- Redux: Conectando a React
- Estado del Servidor: Peticiones, Caché y Sincronización
Módulo 8: Optimización del Rendimiento
- Técnicas de Optimización del Rendimiento en React
- Memorización con React.memo
- Hooks useMemo y useCallback
- División de Código y Carga Perezosa
- Medir el Rendimiento con React DevTools Profiler
Módulo 9: Pruebas en React
- Introducción a las Pruebas
- Pruebas Unitarias con Jest
- Pruebas de Componentes con React Testing Library
- Pruebas de Código Asíncrono y Simulación de APIs
- Pruebas de Extremo a Extremo con Cypress
Módulo 10: Temas Avanzados
- Renderizado del Lado del Servidor (SSR) con Next.js
- Generación de Sitios Estáticos (SSG) con Next.js
- Suspense y React Server Components
- TypeScript con React
- React Native: Creación de Aplicaciones Móviles
