Dominio de hooks: el motor de la interactividad
BorradorURL: https://ruvebal.github.io/web-atelier-udit/lessons/es/react/react-hooks/
📋 Tabla de contenidos
- 🎯 Objetivo del sprint
- 📍 Posición en el viaje
- 🧭 Objetivos de aprendizaje
- 🏗️ Qué construiremos este sprint
- 🔧 Puntos de integración
- 🔑 Conceptos clave: useRef y cleanup en useEffect
- 🌐 La Fetch API: base de useFetch
- 🎓 Metodología: práctica atelier
- 💡 Custom hooks listos para producción
- 🎯 Preguntas críticas: metodología atelier
- 📌 Nota: Memoización en React frente a otros entornos
- 📝 Entregables del sprint
- 🔗 Navegación de la lección
- 📚 Vista previa: conceptos clave
“Un hook es un portal entre el mundo declarativo de React y el mundo imperativo de los efectos.”
🎯 Objetivo del sprint
Al finalizar este sprint: transformar tus componentes estáticos en elementos interactivos “vivos” con estado, efectos y patrones de lógica reutilizable.
📍 Posición en el viaje
| Sprint | Enfoque | Tu app crece |
|---|---|---|
| 5. Fundamentos | Componentes, JSX, Props | Esqueleto de librería de componentes |
| → 6. Hooks | Estado y efectos | Componentes interactivos |
| 7. Arquitectura | Estado global | Features conectadas |
| 8. Routing | Navegación | Estructura multipágina |
🧭 Objetivos de aprendizaje
Al final de esta lección:
- Habrás repasado (o tendrás a mano) el concepto de Promise (MDN) como base de
fetchyasync/await - Conocerás la Fetch API del navegador (
fetch,Response,AbortController) como base de las peticiones HTTP en el hookuseFetch - Usarás
useStatepara estado local - Dominarás
useEffectpara side effects y cleanup - Aplicarás
useRefpara acceso al DOM y valores mutables - Optimizarás con
useMemoyuseCallback - Extraerás lógica reutilizable en custom hooks
- Evitarás pitfalls típicos (closures obsoletos, bucles infinitos)
🏗️ Qué construiremos este sprint
Custom hooks para tu app
// Hooks que crearás en este sprint:
useFetch(url); // → { data, loading, error }
useLocalStorage(key); // → [value, setValue]
useDebounce(value, delay); // → debouncedValue
useToggle(initial); // → [state, toggle, setTrue, setFalse]
useForm(initialValues); // → { values, handleChange, reset }
Estos hooks impulsarán toda tu aplicación.
🔧 Puntos de integración
| Fuente de datos | Uso del hook |
|---|---|
| Laravel API | useFetch para GET, useMutation custom para POST |
| Hygraph CMS | Patrón useQuery para GraphQL (Apollo o custom) |
| Local Storage | useLocalStorage para persistencia (tema, preferencias) |
Preview: patrón de integración con API
// Hook de este sprint...
const { data, loading, error } = useFetch('/api/products');
// ...te prepara para el próximo sprint con React Query
const { data, isLoading, error } = useQuery(['products'], fetchProducts);
🔑 Conceptos clave: useRef y cleanup en useEffect
Antes de construir custom hooks, conviene tener claros dos patrones que usarás una y otra vez.
useRef: acceso al DOM y valores que no disparan re-render
useRef devuelve un objeto { current: valor } que se conserva entre renders.
-
Acceso al DOM: puedes guardar una referencia a un nodo (por ejemplo el
<input>de “nueva tarea”) y usarla de forma imperativa: por ejemploinputRef.current.focus()(.currentes la propiedad donde useRef guarda el valor—aquí, el nodo DOM) para devolver el foco después de enviar un formulario. No necesitas estado para eso: leer o escribir.currentno provoca un re-render. -
Valores que no disparan re-render: si guardas en
.currentalgo que debe persistir entre renders pero no debe redibujar la UI (por ejemplo el últimoAbortControllerde un fetch, un id de timer o un flag “¿es la primera vez?”), React no re-renderiza cuando cambias.current. Por eso useRef sirve para “valores mutables que no son parte de la UI”.
En resumen: useRef = referencia estable al DOM o a un valor que debe vivir entre renders sin provocar re-render.
Diferencias entre useRef y useEffect – React Dev
useEffect con cleanup (timers, suscripciones)
useEffect puede devolver una función. Esa función es el cleanup: React la ejecuta cuando el componente se desmonta o antes de volver a ejecutar el efecto (por cambio de dependencias).
-
Timers: si en un efecto usas
setTimeoutosetInterval, sin cleanup el timer sigue activo aunque el componente ya no esté en pantalla → memory leaks y posibles actualizaciones de estado en un componente desmontado. Por eso en hooks comouseDebounceverásreturn () => clearTimeout(handler);para cancelar el timer al desmontar o cuando cambian las dependencias. -
Suscripciones: lo mismo con suscripciones (eventos, WebSockets, observables): el cleanup debe “darse de baja” (
removeEventListener,unsubscribe, etc.) para no dejar listeners activos.
En una frase: cleanup = “deshacer” lo que hizo el efecto (cancelar timers, desuscribirse) para no dejar trabajo colgado ni actualizar estado en componentes desmontados.
🌐 La Fetch API: base de useFetch
Antes de encapsular la lógica en un hook, conviene entender qué hace el navegador cuando pedimos datos por HTTP. El hook useFetch que construirás usa por debajo la Fetch API: la interfaz nativa del navegador para hacer peticiones y leer respuestas.
Prerequisito: Entender Promises
fetch devuelve una Promise. Si necesitas repasar este concepto antes de continuar, aquí tienes los fundamentos:
¿Qué es una Promise?
Una Promise es un objeto nativo de JavaScript que representa una operación asíncrona. Puede estar en tres estados:
pending(pendiente): la operación aún no ha terminadofulfilled(cumplida): la operación terminó con éxitorejected(rechazada): la operación falló
Tres conceptos que debes distinguir:
- Objeto Promise: representa un valor futuro; tiene métodos como
.then()y.catch() - Función
async: declara una función que siempre devuelve una Promise - Operador
await: pausa la ejecución hasta que la Promise se resuelve (solo funciona dentro de funcionesasync)
Dos formas de trabajar con Promises:
// Forma 1: Usando .then() (estilo tradicional)
fetch('/api/data')
.then((response) => response.json())
.then((data) => console.log(data))
.catch((error) => console.error(error)); // Captura errores
// Forma 2: Usando async/await (estilo moderno, más legible)
async function getData() {
try {
// Bloque try: código que puede fallar
const response = await fetch('/api/data');
const data = await response.json();
console.log(data);
} catch (error) {
// Bloque catch: se ejecuta si hay algún error en try
console.error(error);
}
}
¿Qué hace try...catch?
Es una estructura de control para manejar errores:
try { }: ejecuta el código que puede fallar (como una petición de red)catch (error) { }: captura cualquier error que ocurra en el bloquetryy ejecuta código alternativo
Equivalencia entre las dos formas:
.catch()en Promises ≈catch (error) { }en async/await- Ambas capturan errores, pero
try...catchhace que el código se lea de forma más secuencial
Recursos para profundizar:
- Promise en MDN - Documentación del objeto Promise
- async function - Funciones asíncronas
- await operator - Operador de espera
- .then() method - Método para encadenar acciones
- try…catch – Declaración
Lo mínimo que necesitas saber sobre fetch
Estos cuatro puntos son la base para usar la Fetch API correctamente (y luego construir un hook como useFetch).
1. fetch devuelve una Promise que casi nunca “falla” por HTTP
fetch(url, options?) es una función global. Devuelve una Promise que se resuelve con un objeto Response cuando la petición termina — incluso si el servidor responde 404 o 500. La Promise solo se rechaza por errores de red (sin conexión, CORS, etc.).
Consecuencia: no puedes confiar en que un error HTTP vaya al .catch(). Hay que comprobar el estado de la respuesta a mano.
2. Siempre comprueba el estado antes de leer el cuerpo
Antes de llamar a response.json() (o .text(), .blob()), revisa:
response.ok—truesi el código HTTP está entre 200 y 299response.status— el código numérico (200, 404, 500, etc.)
Si no compruebas y la respuesta es 404 o 500, estarás tratando un error como si fuera éxito (y response.json() puede fallar o devolver un cuerpo de error).
3. El cuerpo de la respuesta se consume una sola vez
response.json(), response.text() o response.blob() devuelven otra Promise y leen el cuerpo del Response. Ese cuerpo es un stream: solo se puede leer una vez. Si llamas dos veces a response.json(), la segunda fallará.
4. Para cancelar la petición: AbortController
Si el componente se desmonta o la URL cambia antes de que llegue la respuesta, querrás cancelar la petición para no actualizar estado en un componente ya desmontado. Para eso se usa la Web API AbortController:
- Creas
new AbortController() - Pasas
{ signal: controller.signal }en las opciones defetch - Cuando quieras cancelar, llamas a
controller.abort()
En un hook como useFetch, típicamente llamas a abort() en la función de cleanup de useEffect.
Ejemplo mínimo (sin React)
Todo lo anterior aplicado en una función reutilizable:
async function getData(url) {
const controller = new AbortController();
const response = await fetch(url, { signal: controller.signal });
// Importante: comprobar estado HTTP antes de leer el cuerpo
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return response.json(); // El cuerpo se consume una sola vez
}
Nota: En un componente React no usarías solo esto. Guardarías controller para llamar a controller.abort() en el cleanup cuando el componente se desmonte o cuando cambie la URL.
Qué añade un hook useFetch sobre esto
Al construir useFetch estarás envolviendo esta lógica y añadiendo:
| Concepto | Qué aporta |
|---|---|
| Estado | data, loading, error para que el componente pueda renderizar según el estado de la petición. |
| Cleanup | En el unmount (o al cambiar dependencias), llamar a controller.abort() para cancelar la petición. |
| Race conditions | Si se lanzan varias peticiones (p. ej. el usuario cambia de página rápido), una respuesta antigua puede llegar después que una reciente. Si actualizas el estado con la respuesta antigua, la UI mostrará datos obsoletos. Por eso se cancelan peticiones anteriores o se ignoran respuestas de requests ya “superadas”. Ver Race condition. |
Para el detalle completo de la Fetch API (métodos, cabeceras, CORS, credenciales, etc.), consulta Using the Fetch API en MDN.
🎓 Metodología: práctica atelier
Ritmo del sprint
┌─────────────────────────────────────────────────────────┐
│ DÍA 1: Deep dive en hooks core │
│ • Patrones useState: primitivos, objetos, arrays │
│ • Ciclo de vida useEffect: mount, update, unmount │
│ • Debug en vivo: React DevTools, consola │
├─────────────────────────────────────────────────────────┤
│ DÍA 2: Taller de custom hooks │
│ • Construir `useFetch` paso a paso │
│ • Equipos crean 2-3 hooks para su app │
│ • Práctica IA: generar tests de hooks con Copilot │
├─────────────────────────────────────────────────────────┤
│ DÍA 3: Integración y pulido │
│ • Conectar hooks a componentes del sprint 5 │
│ • Estados loading/error en la UI │
│ • Peer review: ¿hooks single-responsibility? │
└─────────────────────────────────────────────────────────┘
Protocolo de desarrollo asistido por IA
Prompts concretos para hooks
✅ BUEN PROMPT:
"Crea un custom hook useFetch que:
1. Acepte una URL y opciones opcionales de fetch
2. Devuelva { data, loading, error, refetch }
3. Gestione race conditions (ignora requests antiguas)
4. Haga cleanup al desmontar
5. Devuelva un objeto con data, loading, error y refetch"
❌ MAL PROMPT:
"Haz un fetch hook"
✅ PROMPT DE VALIDACIÓN:
"Revisa este useEffect para:
1. Dependencias faltantes que puedan causar bugs
2. Memory leaks (falta cleanup)
3. Riesgo de bucle infinito
4. Race conditions en operaciones async"
🔍 CUÁNDO NO USAR IA:
- Depurar closures obsoletos (requiere comprensión profunda)
- Decidir entre useCallback y useMemo (hay que perfilar)
- Entender por qué useEffect corre dos veces en dev (React fundamentals)
| Tarea | Rol de la IA | Tu rol |
|---|---|---|
| Depurar dependencias en useEffect | Explicar el warning | Entender el por qué |
| Generar esqueleto de hook | Scaffold de estructura | Añadir manejo de errores |
| Escribir tests de hooks | Borrador de casos | Verificar edge cases |
| Optimizar re-renders | Sugerir memoización | Perfilar antes/después |
💡 Custom hooks listos para producción
Ejemplo 1: useFetch (buenas prácticas)
El siguiente hook usa la Fetch API que vimos arriba y le añade estado de React, cleanup con AbortController y manejo de race conditions.
// hooks/useFetch.js
import { useState, useEffect, useRef } from 'react';
export function useFetch(url, options) {
const [state, setState] = useState({
data: null,
loading: true,
error: null,
});
// Tracking del último request para manejar race conditions
const abortControllerRef = useRef(null);
const fetchData = async () => {
// Cancelar request anterior si sigue pendiente
if (abortControllerRef.current) {
abortControllerRef.current.abort();
}
// El operador new crea nuevo abort controller para este request
// https://developer.mozilla.org/es/docs/Web/JavaScript/Reference/Operators/new
const abortController = new AbortController();
abortControllerRef.current = abortController;
// Estás pasando como Callback a setState una función anónima
// donde prev es el parámetro que recibe el estado actual para poder
// copiarlo y actualizarlo de forma segura con un Spread Operator:
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax
setState((prev) => ({ ...prev, loading: true, error: null }));
try {
const response = await fetch(url, {
...options,
signal: abortController.signal,
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
// Solo actualiza si el request no fue abortado
if (!abortController.signal.aborted) {
setState({ data, loading: false, error: null });
}
} catch (error) {
// Ignorar AbortError
if (error instanceof Error && error.name === 'AbortError') {
return;
}
setState({
data: null,
loading: false,
error: error instanceof Error ? error : new Error('Unknown error'),
});
}
};
useEffect(() => {
fetchData();
// Cleanup: abortar al desmontar
return () => {
if (abortControllerRef.current) {
abortControllerRef.current.abort();
}
};
}, [url]); // Re-fetch si cambia la URL
return { ...state, refetch: fetchData };
}
Uso:
function ProductList() {
const { data, loading, error, refetch } = useFetch('/api/products');
if (loading) return <Spinner />;
if (error) return <ErrorMessage error={error} onRetry={refetch} />;
if (!data) return null;
return (
<div>
<button onClick={refetch}>Actualizar</button>
{data.map((product) => (
<ProductCard key={product.id} product={product} />
))}
</div>
);
}
Ejemplo 2: useLocalStorage
// hooks/useLocalStorage.js
import { useState, useEffect } from 'react';
export function useLocalStorage(key, initialValue) {
// Lee de localStorage o usa initialValue
const [storedValue, setStoredValue] = useState(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.error(`Error reading localStorage key "${key}":`, error);
return initialValue;
}
});
// Setter que persiste en localStorage
const setValue = (value) => {
try {
const valueToStore = value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
window.localStorage.setItem(key, JSON.stringify(valueToStore));
} catch (error) {
console.error(`Error setting localStorage key "${key}":`, error);
}
};
return [storedValue, setValue];
}
Uso:
function ThemeToggle() {
const [theme, setTheme] = useLocalStorage('theme', 'light');
return <button onClick={() => setTheme((t) => (t === 'light' ? 'dark' : 'light'))}>Actual: {theme}</button>;
}
Ejemplo 3: useDebounce
// hooks/useDebounce.js
import { useState, useEffect } from 'react';
export function useDebounce(value, delay = 500) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => {
clearTimeout(handler);
};
}, [value, delay]);
return debouncedValue;
}
Uso:
function SearchInput() {
const [searchTerm, setSearchTerm] = useState('');
const debouncedSearchTerm = useDebounce(searchTerm, 300);
useEffect(() => {
if (debouncedSearchTerm) {
searchAPI(debouncedSearchTerm);
}
}, [debouncedSearchTerm]);
return (
<input type="text" value={searchTerm} onChange={(e) => setSearchTerm(e.target.value)} placeholder="Buscar..." />
);
}
🎯 Preguntas críticas: metodología atelier
Sobre diseño de hooks
💭 Pregunta 1: el dilema del array de dependencias
Tu
useEffecttiene 5 dependencias. ESLint avisa de dependencias faltantes. Si las añades, creas bucles infinitos. Si las quitas, aparecen datos obsoletos.Reflexiona:
- ¿Es señal de que tu effect hace demasiado?
- ¿Cuándo conviene separar un efecto en varios?
- ¿Cómo decides entre
useCallbacky aceptar el re-run?- ¿Qué revela esto del modelo mental de React?
💭 Pregunta 2: abstracción de custom hooks
Has extraído
useFetchpero ahora cada componente necesita algo distinto:
- A necesita caché
- B necesita reintentos
- C necesita cancelación
Reflexiona:
- ¿Lo metes todo en un hook (bloat)?
- ¿Creas 3 hooks (duplicación)?
- ¿Compones hooks (hooks que llaman hooks)?
- ¿Cuándo un hook se convierte en una librería?
💭 Pregunta 3: el escape hatch de useEffect
La doc de React dice: “Quizá no necesitas un efecto”. Pero tu IA te sugiere useEffect para todo.
Reflexiona:
- ¿Cuándo useEffect es la herramienta equivocada?
- ¿Qué puede hacerse durante el render?
- ¿Cómo distingues estado derivado vs sincronizado?
- ¿Por qué React desincentiva efectos?
Sobre desarrollo asistido por IA
💭 Pregunta 4: la trampa de la stale closure
La IA generó este código:
useEffect(() => { const interval = setInterval(() => { setCount(count + 1); // BUG: count está obsoleto }, 1000); return () => clearInterval(interval); }, []);Parece correcto pero falla.
Reflexiona:
- ¿Por qué la IA no vio el bug?
- ¿Cómo desarrollas “intuición de closures”?
- ¿Cuál es el fix? (pista: update funcional)
- ¿Puedes fiarte de la IA en código async/closures?
💭 Pregunta 5: optimización prematura
La IA sugiere envolver todo con
useMemoyuseCallback. Tu app tiene 50 memoizaciones sin problema real medido.Reflexiona:
- ¿Es optimización o ofuscación?
- ¿Cómo mides si la memoización ayudó?
- ¿Cuál es el coste de memoizar?
- ¿Cuándo perfilar antes de optimizar?
Sobre colaboración en atelier
💭 Pregunta 6: divergencia de patrones de hooks
Tu equipo tiene 3 hooks de fetch distintos:
useFetch(tuyo)useAPI(compañera A)useData(compañera B)Todos hacen cosas parecidas, diferente.
Reflexiona:
- ¿Cómo consolidar sin herir sensibilidades?
- ¿Qué hace que un patrón sea “mejor”?
- ¿Debe el equipo estandarizar o puede haber diversidad?
- ¿Cómo se gestiona esto en equipos reales?
💭 Pregunta 7: la curva de aprendizaje
Una compañera pregunta: “¿Por qué mi useEffect corre dos veces?” Sabes que es React Strict Mode, pero está frustrada.
Reflexiona:
- ¿Cómo lo explicas sin condescendencia?
- ¿Cuál es el valor pedagógico de este comportamiento?
- ¿Debería empezar por hooks o por clases?
- ¿Cómo enseñas el “por qué”, no solo el “cómo”?
📌 Nota: Memoización en React frente a otros entornos
Cuando trabajas con useMemo, useCallback y React.memo, es natural preguntarse: ¿por qué en React tenemos que preocuparnos tanto por las referencias? Esta nota sitúa el diseño de React en contexto.
Por qué React depende de la referencia
En JavaScript la igualdad es por referencia (===). Cada vez que el componente se re-renderiza, una función o un array creados en el render son nuevos en memoria: mismo comportamiento, distinta referencia. Para React (y para React.memo) eso cuenta como “props cambiadas”, así que el hijo se re-renderiza. Por eso usamos useCallback y useMemo: no para evitar que el padre re-renderice, sino para mantener la misma referencia cuando el valor lógico no ha cambiado, de modo que los hijos memoizados no reciban “nuevas” props y eviten trabajo innecesario.
Cómo lo abordan otros lenguajes y frameworks
| Enfoque | Ejemplo | Idea clave |
|---|---|---|
| Igualdad estructural | Clojure, Elm, Rust (con traits de igualdad) | Dos valores “iguales en contenido” se consideran iguales aunque sean referencias distintas. El framework puede decidir si recalcular o no sin que tú estabilices referencias a mano. |
| Compilador o runtime que rastrea dependencias | Svelte, Vue (ref/computed), SwiftUI |
Svelte analiza qué variables usa cada bloque y genera código que solo se re-ejecuta cuando esas variables cambian; no escribes useMemo ni useCallback. Vue y SwiftUI encapsulan de forma similar la noción de “de qué depende esto”. |
| Datos inmutables por defecto | Elm, ClojureScript | Los datos no se mutan; la igualdad suele ser estructural. “¿Ha cambiado?” se resuelve por valor, no por referencia, y el problema de “misma función, otra referencia” no se plantea igual. |
Conclusión
No es que “JavaScript sea así y haya que aceptarlo”: React eligió un modelo explícito en el que tú controlas la identidad (referencias) y cuándo optimizar. Eso hace el modelo muy enseñable y flexible, pero obliga a pensar en referencias y a usar useCallback/useMemo/memo cuando quieres evitar re-renders o trabajo redundante. En otros ecosistemas esa preocupación suele quedar oculta tras igualdad estructural o un compilador/runtime que infiere dependencias. Conocer ambos enfoques ayuda a explicar por qué en React la memoización es parte del diseño, no un capricho del lenguaje.
📝 Entregables del sprint
- 3+ custom hooks (
useFetch,useLocalStorage,useDebounce) - Feature interactiva usando useState (p. ej., form, toggle)
- Cleanup en al menos un useEffect
- Tests de hooks al menos para
useFetch - Reflexión respondiendo 3+ preguntas críticas
- Auditoría de dependencias - documenta por qué cada dependencia es necesaria
- Peer code review enfocada en patrones de hooks y posibles bugs
🔗 Navegación de la lección
| Anterior | Actual | Siguiente |
|---|---|---|
| Fundamentos de React | Dominio de hooks | Arquitectura de estado |
📚 Vista previa: conceptos clave
Contenido completo pendiente. Temas incluidos:
- Reglas de los hooks (y por qué existen)
- Patrones y pitfalls de useState
- useEffect: modelo mental
- Cleanup y memory leaks
- useRef más allá del DOM
- Rendimiento: useMemo y useCallback (véase la nota sobre memoización frente a otros entornos)
- Construir custom hooks
- Testing de hooks
“Cada custom hook es una pieza de sabiduría reutilizable, extraída del caos de un componente.”