🎯 Objetivo del tema
El administrador aún no ha insertado un video para esta sección.
🗺️ Mapa del tema
Antes de los Hooks (2018), los componentes React se escribían como CLASES con this.state, this.setState, this.componentDidMount. Era verboso, difícil de reutilizar lógica, y this en JavaScript es una fuente infinita de bugs. Los Hooks llegaron para resolver TODO eso: permiten usar estado y otras features de React sin clases, con funciones simples. Hoy el 99% del código React moderno usa Hooks. Si los dominas, dominas React.
Por qué los Hooks son especiales y dónde NO debes usarlos.
Cómo ejecutar código después del render: fetches, suscripciones, timers.
Acceder a inputs, hacer focus, integrar librerías de terceros.
Cuándo (no) usar memoización para evitar renders innecesarios.
Tu primera abstracción real en React: extraer lógica a una función.
Proyecto completo: filtrado en vivo, paginación, y carga de imágenes.
3.2.1 Las 2 reglas de oro de los Hooks
Los Hooks son funciones especiales que te permiten usar estado y otras features de React sin escribir clases. Tienen 2 reglas ABSOLUTAS que NUNCA debes violar: solo se llaman en el top level de un componente o custom hook, y solo en componentes funcionales o custom hooks. Si las rompes, React se comporta de formas extrañas. Es la base de todo lo demás.
- Regla #1: SOLO en el top level. NUNCA dentro de if, for, while, funciones anidadas, ni try/catch.
- Regla #2: SOLO en componentes funcionales de React o en otros Hooks personalizados (custom hooks). NUNCA en funciones normales de JavaScript.
// ✅ CORRECTO: useState en el top level
function Contador() {
const [count, setCount] = useState(0);
const [loading, setLoading] = useState(false);
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}
// ❌ INCORRECTO: useState dentro de un if (rompe la regla #1)
function Contador() {
if (Math.random() > 0.5) {
const [count, setCount] = useState(0); // ERROR: hook en condicional
}
return <p>Hola</p>;
}
// ❌ INCORRECTO: useState en una función normal (rompe la regla #2)
function contar() {
const [count, setCount] = useState(0); // ERROR: no es componente ni hook
return count;
}
// ✅ CORRECTO: Hook en un custom hook
function useContadorInicial(valorInicial) {
const [count, setCount] = useState(valorInicial);
return { count, incrementar: () => setCount(c => c + 1) };
}
function MiComponente() {
const { count, incrementar } = useContadorInicial(0); // OK, custom hook
return <button onClick={incrementar}>{count}</button>;
}Las 2 reglas de los Hooks: correcto vs incorrecto
El administrador aún no ha insertado un video para esta sección.
3.2.2 useEffect: el hook más poderoso
useEffect es para EFECTOS SECUNDARIOS: todo lo que pasa 'después' del render. Fetch a una API, suscribirte a eventos, iniciar un timer, modificar el DOM manualmente. Es la traducción moderna de los lifecycle methods de clase (componentDidMount, componentDidUpdate, componentWillUnmount), pero con una API más simple. Lo usarás en el 80% de tus componentes que interactúan con el mundo exterior.
Las 3 formas de useEffect según dependencias
// 1. SIN array de dependencias: se ejecuta en CADA render (casi siempre MALO)
useEffect(() => {
console.log('Se ejecuta en cada render');
}); // ⚠️ posible loop infinito si hace fetch
// 2. Con array VACÍO []: se ejecuta SOLO al montar (fetch inicial)
useEffect(() => {
fetch('/api/datos').then(r => r.json()).then(setData);
}, []); // ✅ patrón para cargar datos iniciales
// 3. Con dependencias [dep1, dep2]: se ejecuta cuando cambian esas deps
useEffect(() => {
document.title = `Hola, ${nombre}`; // actualiza el título cuando cambia nombre
}, [nombre]); // ✅ se ejecuta solo cuando nombre cambia
// 4. Con función de CLEANUP (return): para limpiar el efecto al desmontar
useEffect(() => {
const timer = setInterval(() => console.log('tick'), 1000);
return () => clearInterval(timer); // cleanup: se ejecuta al desmontar
}, []);Las 4 formas de useEffect
Ejemplo completo: contador con título dinámico
import { useState, useEffect } from 'react';
function ContadorConTitulo() {
const [count, setCount] = useState(0);
// Efecto 1: actualizar el título de la página cuando cambia count
useEffect(() => {
document.title = `Contador: ${count} clics`;
}, [count]); // se ejecuta cuando count cambia
// Efecto 2: log al montar (solo una vez)
useEffect(() => {
console.log('Componente montado');
return () => console.log('Componente desmontado'); // cleanup
}, []); // [] = solo al montar/desmontar
return (
<div>
<h1>{count}</h1>
<button onClick={() => setCount(count + 1)}>+1</button>
</div>
);
}useEffect con cleanup y dependencias
El administrador aún no ha insertado un video para esta sección.
3.2.3 useRef: referencias al DOM sin re-renderizar
useRef te da una CAJA que persiste entre renders SIN causar re-render. Es perfecta para: acceder al DOM directamente (focus en un input, scroll a una sección, reproducir video), guardar valores que no deben causar render (timers, contadores), integrar librerías de terceros. Es el hook de los detalles de bajo nivel: cuando useState es demasiado, useRef es lo que necesitas.
import { useRef, useEffect } from 'react';
// 1. Focus automático en un input al cargar
function BuscadorAutoFocus() {
const inputRef = useRef(null);
useEffect(() => {
inputRef.current.focus(); // hace focus en el input al montar
}, []);
return <input ref={inputRef} placeholder="Buscar..." />;
}
// 2. Guardar el valor anterior de un estado (sin causar render)
function ContadorConAnterior() {
const [count, setCount] = useState(0);
const prevCountRef = useRef(0);
useEffect(() => {
prevCountRef.current = count; // se actualiza sin re-render
});
return (
<div>
<p>Actual: {count}</p>
<p>Anterior: {prevCountRef.current}</p>
<button onClick={() => setCount(count + 1)}>+</button>
</div>
);
}
// 3. Acceso a un elemento de video para controlarlo
function ReproductorVideo() {
const videoRef = useRef(null);
const play = () => videoRef.current.play();
const pause = () => videoRef.current.pause();
return (
<div>
<video ref={videoRef} src="video.mp4" />
<button onClick={play}>Play</button>
<button onClick={pause}>Pause</button>
</div>
);
}
// Regla de useRef: la mutación de .current NO causa re-render.
// Es diferente de useState. Si necesitas que la UI se actualice, usa useState.useRef: 3 usos comunes (focus, valor anterior, DOM)
El administrador aún no ha insertado un video para esta sección.
3.2.4 useMemo y useCallback: optimización
useMemo y useCallback son hooks de OPTIMIZACIÓN: memorizan (recuerdan) un valor o función para no recalcularlo/recrearlo en cada render. Son útiles SOLO cuando tienes un cálculo costoso o pasas funciones a componentes memoizados. Si los usas sin necesidad, solo agregas complejidad. La regla: primero mide, después optimiza.
useMemo: memoriza un valor calculado
import { useMemo, useState } from 'react';
// SIN useMemo: el filtro se ejecuta en CADA render (lento si hay 10000 items)
function ListaConFiltro({ items, termino }) {
const [extra, setExtra] = useState(0);
// Este filter se ejecuta en CADA render, incluso cuando cambia 'extra'
const filtrados = items.filter(item => item.name.includes(termino));
return <>{filtrados.length} items</>;
}
// CON useMemo: el filtro SOLO se ejecuta cuando items o termino cambian
function ListaConFiltroOptimizado({ items, termino }) {
const [extra, setExtra] = useState(0);
// Solo se recalcula si items o termino cambian (no cuando cambia 'extra')
const filtrados = useMemo(
() => items.filter(item => item.name.includes(termino)),
[items, termino] // dependencias: solo recalcular si estas cambian
);
return <>{filtrados.length} items</>;
}useMemo: cuándo y por qué
useCallback: memoriza una función
import { useCallback, useState, memo } from 'react';
// Componente hijo memoizado: solo se re-renderiza si sus props cambian
const Boton = memo(function Boton({ onClick, children }) {
console.log('Boton renderizado');
return <button onClick={onClick}>{children}</button>;
});
// SIN useCallback: handleClick es una función NUEVA en cada render → Boton se re-renderiza
function PadreSinCallback() {
const [count, setCount] = useState(0);
const handleClick = () => console.log('click'); // función nueva cada render
return (
<div>
<button onClick={() => setCount(count + 1)}>Contador: {count}</button>
<Boton onClick={handleClick}>Click me</Boton>
</div>
);
}
// CON useCallback: handleClick es la MISMA función si count no cambia → Boton NO se re-renderiza
function PadreConCallback() {
const [count, setCount] = useState(0);
const handleClick = useCallback(() => console.log('click'), []); // función memorizada
return (
<div>
<button onClick={() => setCount(count + 1)}>Contador: {count}</button>
<Boton onClick={handleClick}>Click me</Boton>
</div>
);
}useCallback: para componentes memoizados
El administrador aún no ha insertado un video para esta sección.
3.2.5 Custom Hooks: reutilizar lógica
Los Custom Hooks son la forma de extraer lógica reutilizable entre componentes. Es la primera abstracción REAL de React: en vez de copiar useState + useEffect en cada componente que hace fetch, creas un useFetch que devuelve { data, loading, error }. Es como una función que encapsula lógica de React. Si lo entiendes, empiezas a pensar como un senior.
Custom hook #1: useFetch (reutilizable en TODA tu app)
import { useState, useEffect } from 'react';
// Custom hook: encapsula la lógica de fetch
function useFetch(url) {
const [data, setData] = useState(null);
const [cargando, setCargando] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let cancelado = false; // para evitar setState si el componente se desmonta
setCargando(true);
fetch(url)
.then(r => {
if (!r.ok) throw new Error(`HTTP ${r.status}`);
return r.json();
})
.then(data => { if (!cancelado) setData(data); })
.catch(err => { if (!cancelado) setError(err.message); })
.finally(() => { if (!cancelado) setCargando(false); });
return () => { cancelado = true; }; // cleanup
}, [url]); // re-fetch si cambia la URL
return { data, cargando, error };
}
// Uso en cualquier componente (¡3 líneas!):
function ListaDePersonajes() {
const { data, cargando, error } = useFetch('https://rickandmortyapi.com/api/character');
if (cargando) return <p>Cargando...</p>;
if (error) return <p>Error: {error}</p>;
return <div>{data.results.map(char => <p key={char.id}>{char.name}</p>)}</div>;
}
function DetalleDePersonaje({ id }) {
const { data, cargando, error } = useFetch(`https://rickandmortyapi.com/api/character/${id}`);
if (cargando) return <p>Cargando...</p>;
if (error) return <p>Error: {error}</p>;
return <h1>{data.name}</h1>;
}
// La misma lógica, 2 componentes diferentes. SIN useFetch, tendrías que duplicar ~20 líneas de useState+useEffect en cada uno.Custom hook useFetch: reutiliza lógica de fetch
Custom hook #2: useLocalStorage (sincroniza con localStorage)
import { useState, useEffect } from 'react';
function useLocalStorage(key, valorInicial) {
// Leer de localStorage al iniciar (lazy initializer)
const [valor, setValor] = useState(() => {
try {
const stored = localStorage.getItem(key);
return stored ? JSON.parse(stored) : valorInicial;
} catch {
return valorInicial;
}
});
// Sincronizar con localStorage cada vez que cambia
useEffect(() => {
try {
localStorage.setItem(key, JSON.stringify(valor));
} catch (err) {
console.error('Error guardando en localStorage:', err);
}
}, [key, valor]);
return [valor, setValor];
}
// Uso: igual que useState, pero persiste automáticamente
function PreferenciasUsuario() {
const [tema, setTema] = useLocalStorage('tema', 'claro');
const [nombre, setNombre] = useLocalStorage('nombre', '');
return (
<div>
<select value={tema} onChange={e => setTema(e.target.value)}>
<option value="claro">Claro</option>
<option value="oscuro">Oscuro</option>
</select>
<input value={nombre} onChange={e => setNombre(e.target.value)} />
<p>Tu tema: {tema}, tu nombre: {nombre}</p>
</div>
);
}
// Cierra el navegador, ábrelo de nuevo: el tema y nombre siguen ahí.
// ¡Persistencia automática sin escribir lógica extra!Custom hook useLocalStorage: persistencia automática
El administrador aún no ha insertado un video para esta sección.
3.2.6 useEffect con APIs reales: REST Countries + búsqueda
Proyecto integrador: vamos a construir un buscador de países en vivo usando la API pública REST Countries (https://restcountries.com/), que es 100% gratis, sin auth, y devuelve datos de TODOS los países del mundo (nombre, capital, población, bandera, idiomas, etc.). Vas a combinar: useState para los datos y la búsqueda, useEffect para cargar inicialmente, useMemo para filtrar eficientemente, y debounce para no filtrar en cada tecla (optimización).
API pública: REST Countries (gratis, sin auth)
- Endpoint: https://restcountries.com/v3.1/all?fields=name,capital,population,flag,languages,region
- Sin auth ni API key: solo haz fetch.
- Devuelve array de 250+ países con datos completos.
- Documentación: https://restcountries.com/
- Búsqueda por nombre: https://restcountries.com/v3.1/name/peru
- Filtro por región: https://restcountries.com/v3.1/region/americas
El proyecto completo: buscador de países
import { useState, useEffect, useMemo } from 'react';
function PaisesApp() {
const [paises, setPaises] = useState([]);
const [busqueda, setBusqueda] = useState('');
const [region, setRegion] = useState('all');
const [cargando, setCargando] = useState(true);
const [error, setError] = useState(null);
// Cargar países al montar
useEffect(() => {
fetch('https://restcountries.com/v3.1/all?fields=name,capital,population,flag,languages,region')
.then(r => r.json())
.then(data => setPaises(data))
.catch(err => setError(err.message))
.finally(() => setCargando(false));
}, []);
// Filtrar con useMemo (solo se recalcula si cambia busqueda, region, o paises)
const paisesFiltrados = useMemo(() => {
return paises.filter(p => {
const matchNombre = p.name.common.toLowerCase().includes(busqueda.toLowerCase());
const matchRegion = region === 'all' || p.region === region;
return matchNombre && matchRegion;
});
}, [paises, busqueda, region]);
if (cargando) return <p>Cargando 250+ países...</p>;
if (error) return <p>Error: {error}</p>;
return (
<div className="paises-app">
<h1>🌍 Buscador de Países ({paisesFiltrados.length} resultados)</h1>
<input
type="text"
placeholder="Buscar país..."
value={busqueda}
onChange={e => setBusqueda(e.target.value)}
/>
<select value={region} onChange={e => setRegion(e.target.value)}>
<option value="all">Todas las regiones</option>
<option value="Americas">Américas</option>
<option value="Europe">Europa</option>
<option value="Asia">Asia</option>
<option value="Africa">África</option>
<option value="Oceania">Oceanía</option>
</select>
<div className="paises-grid">
{paisesFiltrados.map(p => (
<article key={p.cca3} className="pais-card">
<img src={p.flags.png} alt={p.name.common} />
<h2>{p.name.common}</h2>
<p><strong>Capital:</strong> {p.capital?.[0]}</p>
<p><strong>Población:</strong> {p.population.toLocaleString()}</p>
<p><strong>Región:</strong> {p.region}</p>
</article>
))}
</div>
</div>
);
}Buscador de países con REST Countries, useState, useEffect, useMemo
El administrador aún no ha insertado un video para esta sección.
📚 Contenido ampliado
Material adicional, referencias externas verificadas y ejemplos extendidos.
🤖 AI Mission
Los Hooks se dominan construyendo, no leyendo.
Misión: Crea una app de 'Favoritos del clima' con React: una lista de ciudades favoritas que muestra el clima actual de cada una usando la API pública Open-Meteo (gratis, sin API key). Usa useState, useEffect, useMemo, y al menos 1 custom hook. Pídele a la IA que revise tu código. Anota en tu cuaderno: ¿cuál hook te costó más entender?
Pasos sugeridos
- Crea el proyecto: npm create vite@latest clima-favoritos -- --template react.
- Crea hooks/useClima.js (custom hook): recibe una ciudad, hace fetch a Open-Meteo, devuelve { data, cargando, error }.
- API Open-Meteo: https://api.open-meteo.com/v1/forecast?latitude=-34.6&longitude=-58.4¤t_weather=true (no necesita API key, devuelve el clima actual).
- Crea components/ClimaCard.jsx: recibe { ciudad, data, cargando, error } y muestra: nombre, temperatura actual, velocidad del viento, hora.
- Crea components/ListaCiudades.jsx: array hardcoded de 5 ciudades (Buenos Aires, Madrid, CDMX, Tokio, Londres) con sus coordenadas.
- App.jsx: lista las 5 ciudades usando el custom hook useClima para cada una.
- Maneja los 3 estados: cargando (spinner), error (mensaje), éxito (datos del clima).
- Estiliza: grid responsive, tarjetas con color según temperatura (>25°C naranja, <10°C azul).
- Agrega useMemo para que el cálculo de 'color según temperatura' solo se ejecute cuando cambian los datos.
- Pídele a la IA: 'Tengo esta app del clima. Sugiere 2 mejoras: features (búsqueda, gráfico, pronóstico 7 días) o performance (useCallback). NO me des código, dime QUÉ agregar.'
- Aplica 2 mejoras. Vuelve a probar.
- Anota: 3 hooks que entendiste mejor al HACER, y 1 que te sigue costando.
📓 Entregable: Capturas de: la app mostrando el clima de 5 ciudades, tarjetas con colores según temperatura, código de useClima.js, y media página de cuaderno con tu reflexión.
🚫 Errores típicos de razonamiento
Por qué: Si escribes useEffect(() => { fetch(...).then(setData) }, ) SIN el array, el efecto se ejecuta en CADA render. Si setData causa un nuevo render (que sí, por eso lo llamaste), tienes un loop infinito. SIEMPRE pon el array: [] para solo al montar, [dep] cuando cambien las deps. Es uno de los errores más sutiles de React.
Por qué: React te avisa con 'React Hook useEffect has a missing dependency'. Si lo ignoras con eslint-disable, el efecto NUNCA se vuelve a ejecutar aunque cambien las variables. La regla: TODA variable externa que uses dentro del efecto va en el array. Es una de las reglas de exhaustividad que ESLint enforce automáticamente.
Por qué: useState causa re-render cuando cambia. useRef NO causa re-render. Si necesitas que la UI se actualice cuando el valor cambia, usa useState. Si solo necesitas el valor sin causar render (focus, scroll, ID de timer), usa useRef. Confundirlos es el error #1 de los Hooks.
Por qué: useMemo y useCallback agregan complejidad y consumen memoria. Si tu componente tiene 50 items y no notas lag, no los uses. La regla: primero mide con React DevTools Profiler, después optimiza. Optimización prematura es la raíz de todo mal en software.
Por qué: Los Hooks deben estar en el top level del componente. React los identifica por ORDEN de llamada: si metes un useState dentro de un if que a veces se ejecuta y a veces no, React pierde la cuenta y los Hooks se desordenan. El linter react-hooks te avisa, pero la causa es: SIEMPRE Hooks en el top level, NUNCA dentro de condicionales.
🧪 Laboratorio práctico
Los Hooks se aprenden construyendo, no leyendo.
Laboratorio: App de países del mundo con REST Countries + Hooks
Objetivo: Construir un buscador de países con React + Vite + REST Countries API + Hooks (useState, useEffect, useMemo). La app debe: cargar 250+ países, tener búsqueda en vivo por nombre, filtro por región, mostrar bandera + capital + población. Practicarás todos los Hooks fundamentales.
Pasos
- Crea el proyecto: npm create vite@latest paises-app -- --template react.
- Crea components/PaisCard.jsx: recibe { pais }, muestra bandera, nombre, capital, población, región, idiomas.
- Crea components/Buscador.jsx: input controlado con useState para el término de búsqueda.
- Crea components/FiltroRegion.jsx: select con useState para la región (Todas, Américas, Europa, Asia, África, Oceanía).
- Crea components/ListaPaises.jsx: useState para paises, useEffect para fetch inicial, useMemo para filtrar por búsqueda y región.
- App.jsx: orquesta los componentes en un layout limpio.
- Estiliza con CSS Grid responsive: 1 col en móvil, 2 en tablet, 3-4 en desktop.
- Maneja los 3 estados: cargando (skeleton), error (mensaje), éxito (grid).
- Prueba: busca 'perú' (1 resultado), 'united' (varios), 'xyz' (sin resultados). Filtra por 'Americas'.
- Verifica useMemo: en DevTools > React Profiler, confirma que el filtro solo se ejecuta cuando cambia busqueda o region, no en cada render.
- Sube a Vercel/Netlify. Comparte URL.
- Haz commit: 'feat: app de países con REST Countries, useState, useEffect, useMemo'.
📓 Entregable: URL pública de la app, capturas de: 250+ países cargados, búsqueda 'peru' filtrando, filtro por región, código de los 4 componentes + custom hook si lo usaste, y commit en Git.