React avanzado y arquitectura de componentes
Contenido
- Introducción
- Conocimiento previo
- Referencias
- Índice
- 1. Qué cambia al pasar de React básico a React avanzado
- 2. Diseñar componentes con responsabilidad clara
- 3. Estado local, estado compartido y estado derivado
- 4. Actualizar estructuras de estado sin mutarlas
- 5. useReducer para lógica de estado más compleja
- 6. useEffect como sincronización con sistemas externos
- 7. Cuándo no necesitas un efecto
- 8. Peticiones remotas con estados de carga y error
- 9. Hooks personalizados para reutilizar lógica
- 10. Contexto para datos compartidos entre componentes lejanos
- 11. Refs para acceder a valores o elementos externos al renderizado
- 12. Formularios con validación y mensajes accesibles
- 13. Organización de carpetas en una aplicación React
- 14. Introducción a rutas en aplicaciones cliente
- 15. Accesibilidad en componentes interactivos
- 16. Rendimiento sin sobreoptimizar
- 17. TypeScript en componentes y hooks
- 18. Proyecto guiado: catálogo con filtros, favoritos y datos remotos
- 18.1. Definir la estructura del proyecto
- 18.2. Crear un servicio de datos
- 18.3. Crear un hook para cargar productos
- 18.4. Gestionar filtros con useReducer
- 18.5. Compartir favoritos con contexto
- 18.6. Componer la pantalla principal
- 19. Criterios para elegir una solución
- 20. Ejercicios
Unidad didáctica avanzada de React para Desarrollo web en entorno cliente: arquitectura de componentes, estado compartido, useReducer, useEffect, hooks personalizados, contexto, refs, datos remotos, accesibilidad, rendimiento y proyecto guiado.
Introducción
En la unidad anterior aprendiste los fundamentos de React: componentes, JSX, props, estado, eventos, listas y formularios controlados. Con esos conceptos ya puedes construir interfaces pequeñas. Sin embargo, cuando una aplicación crece aparecen nuevas preguntas: dónde debe vivir el estado, cómo evitar componentes demasiado grandes, cuándo usar efectos, cómo reutilizar lógica, cómo compartir datos entre componentes lejanos y cómo organizar el proyecto para que siga siendo mantenible.
Esta unidad profundiza en React desde una perspectiva práctica. No busca introducir muchas librerías ni patrones complejos, sino ayudarte a tomar decisiones razonables en aplicaciones medianas. El objetivo es que sepas estructurar componentes, sincronizar la interfaz con APIs o sistemas externos, extraer hooks personalizados y preparar un proyecto más sólido sin perder claridad.
React avanzado no significa escribir código más complicado. Significa saber cuándo una solución simple deja de ser suficiente y qué herramienta de React conviene usar en cada caso.
Conocimiento previo
- React: fundamentos para aplicaciones cliente
- JavaScript
- JavaScript en el navegador: DOM, eventos, formularios y comunicación asíncrona
- Uso básico de
fetch, promesas,asyncyawait. - Comprensión de arrays, objetos, funciones, módulos, formularios y eventos.
Referencias
- React - Managing State
- React - You Might Not Need an Effect
- React - Synchronizing with Effects
- React - Reusing Logic with Custom Hooks
- React - Passing Data Deeply with Context
- React - Referencing Values with Refs
- React - useReducer
- React - useDeferredValue
- React - useTransition
- MDN - Fetch API
- TypeScript Handbook
Índice
- Qué cambia al pasar de React básico a React avanzado
- Diseñar componentes con responsabilidad clara
- Estado local, estado compartido y estado derivado
- Actualizar estructuras de estado sin mutarlas
- useReducer para lógica de estado más compleja
- useEffect como sincronización con sistemas externos
- Cuándo no necesitas un efecto
- Peticiones remotas con estados de carga y error
- Hooks personalizados para reutilizar lógica
- Contexto para datos compartidos entre componentes lejanos
- Refs para acceder a valores o elementos externos al renderizado
- Formularios con validación y mensajes accesibles
- Organización de carpetas en una aplicación React
- Introducción a rutas en aplicaciones cliente
- Accesibilidad en componentes interactivos
- Rendimiento sin sobreoptimizar
- TypeScript en componentes y hooks
- Proyecto guiado: catálogo con filtros, favoritos y datos remotos 18.1. Definir la estructura del proyecto 18.2. Crear un servicio de datos 18.3. Crear un hook para cargar productos 18.4. Gestionar filtros con useReducer 18.5. Compartir favoritos con contexto 18.6. Componer la pantalla principal
- Criterios para elegir una solución
- Ejercicios
1. Qué cambia al pasar de React básico a React avanzado
En React básico normalmente trabajas con pocos componentes y con estado local sencillo. Un contador, una lista o un formulario pueden resolverse con useState y props directas. En una aplicación más grande, el problema ya no es solo conseguir que algo funcione, sino mantener el código comprensible cuando aparecen más pantallas, más datos y más interacciones.
Los cambios principales son estos:
| React básico | React avanzado |
|---|---|
| Componentes pequeños con pocos datos. | Árbol de componentes con responsabilidades repartidas. |
Estado local con useState. | Estado local, compartido, derivado y lógica con useReducer. |
| Props directas entre padre e hijo. | Props, composición y contexto cuando hay componentes lejanos. |
| Formularios simples. | Validación, mensajes accesibles y control de envío. |
| Peticiones puntuales. | Estados de carga, error, cancelación lógica y hooks de datos. |
| Organización informal. | Separación de componentes, hooks, servicios y utilidades. |
La idea clave es que React no te obliga a usar siempre la misma solución. Puedes empezar con useState y props. Si la lógica crece, puedes extraer componentes, crear un hook personalizado, usar useReducer o compartir datos con contexto.
2. Diseñar componentes con responsabilidad clara
Un componente tiene una responsabilidad clara cuando puedes explicar su trabajo en una frase. Por ejemplo: ListaProductos muestra una colección de productos, FiltroCategoria permite elegir una categoría y ProductoCard representa un producto individual.
Un síntoma de mala organización es un componente App enorme que carga datos, guarda todos los estados, valida formularios, filtra listas, renderiza tarjetas y muestra mensajes. Ese componente puede funcionar, pero será difícil de leer y de modificar.
Ejemplo poco mantenible:
function App() { const [productos, setProductos] = useState([]); const [busqueda, setBusqueda] = useState(''); const [categoria, setCategoria] = useState('todas'); const [favoritos, setFavoritos] = useState([]); const [cargando, setCargando] = useState(false); const [error, setError] = useState(null);
// Carga datos, filtra, valida, renderiza formularios y tarjetas...}Una organización más clara separa responsabilidades:
function CatalogoPage() { const { productos, cargando, error } = useProductos(); const [filtros, dispatch] = useReducer(filtrosReducer, filtrosIniciales);
const productosFiltrados = filtrarProductos(productos, filtros);
return ( <main> <CabeceraCatalogo total={productosFiltrados.length} /> <PanelFiltros filtros={filtros} dispatch={dispatch} /> <EstadoCarga cargando={cargando} error={error} /> <ListaProductos productos={productosFiltrados} /> </main> );}No se trata de dividir por dividir. Un componente nuevo aporta valor cuando:
- Reduce una parte del código que estaba creciendo demasiado.
- Tiene una responsabilidad que puede nombrarse con claridad.
- Se reutiliza o puede probarse mentalmente de forma aislada.
- Evita mezclar lógica de datos con representación visual.
- Hace más fácil localizar dónde se produce un cambio.
3. Estado local, estado compartido y estado derivado
Antes de elegir una herramienta, conviene identificar qué tipo de estado tienes.
| Tipo de estado | Qué significa | Ejemplo |
|---|---|---|
| Estado local | Solo lo necesita un componente. | Si un menú está abierto o cerrado. |
| Estado compartido | Lo necesitan varios componentes. | Lista de productos o usuario actual. |
| Estado derivado | Se calcula a partir de otros datos. | Productos filtrados según búsqueda. |
| Estado remoto | Procede de una API o sistema externo. | Pedidos descargados desde servidor. |
El estado local debe vivir cerca del componente que lo usa. Si solo un componente necesita saber si un panel está desplegado, no tiene sentido subir ese estado a toda la aplicación.
function PanelAyuda() { const [abierto, setAbierto] = useState(false);
return ( <section> <button onClick={() => setAbierto(!abierto)}>Ayuda</button> {abierto && <p>Introduce un nombre y un precio válido.</p>} </section> );}El estado compartido debe subir al padre común más cercano. Si dos componentes hermanos necesitan leer o modificar el mismo dato, ese dato no debe duplicarse en ambos.
function ProductosPage() { const [busqueda, setBusqueda] = useState('');
return ( <> <Buscador value={busqueda} onChange={setBusqueda} /> <ResumenBusqueda busqueda={busqueda} /> </> );}El estado derivado no suele guardarse con useState. Si puedes calcularlo durante el renderizado a partir de datos existentes, calcúlalo directamente.
function ListaProductos({ productos, busqueda }) { const productosFiltrados = productos.filter((producto) => producto.nombre.toLowerCase().includes(busqueda.toLowerCase()), );
return ( <ul> {productosFiltrados.map((producto) => ( <li key={producto.id}>{producto.nombre}</li> ))} </ul> );}Guardar productosFiltrados en otro estado puede generar incoherencias: tendrías dos fuentes de verdad, la lista original y la lista filtrada.
4. Actualizar estructuras de estado sin mutarlas
React espera que trates el estado como inmutable. Esto significa que no debes modificar directamente arrays u objetos guardados en estado. En su lugar, crea una nueva versión con los cambios.
No hagas esto:
function marcarFavorito(id) { producto.favorito = true; setProducto(producto);}Sí haz esto:
function marcarFavorito(id) { setProductos((productosActuales) => productosActuales.map((producto) => producto.id === id ? { ...producto, favorito: true } : producto, ), );}Patrones habituales:
| Operación | Patrón recomendado |
|---|---|
| Añadir elemento | [...lista, nuevoElemento] |
| Eliminar elemento | lista.filter((item) => item.id !== id) |
| Modificar elemento | lista.map((item) => item.id === id ? actualizado : item) |
| Copiar objeto | { ...objeto, propiedad: nuevoValor } |
Estos patrones ayudan a React a detectar que el estado ha cambiado y hacen que tu código sea más predecible.
5. useReducer para lógica de estado más compleja
useState funciona bien cuando el estado es simple. Pero si una pantalla tiene muchas acciones relacionadas, varios campos o reglas de actualización, useReducer puede hacer el código más claro.
Un reducer es una función que recibe el estado actual y una acción, y devuelve el siguiente estado.
const estadoInicial = { busqueda: '', categoria: 'todas', soloFavoritos: false,};
function filtrosReducer(estado, accion) { if (accion.type === 'cambiar-busqueda') { return { ...estado, busqueda: accion.valor }; }
if (accion.type === 'cambiar-categoria') { return { ...estado, categoria: accion.valor }; }
if (accion.type === 'alternar-favoritos') { return { ...estado, soloFavoritos: !estado.soloFavoritos }; }
return estado;}Uso en un componente:
function PanelFiltros() { const [filtros, dispatch] = useReducer(filtrosReducer, estadoInicial);
return ( <form> <label htmlFor="busqueda">Buscar</label> <input id="busqueda" value={filtros.busqueda} onChange={(event) => dispatch({ type: 'cambiar-busqueda', valor: event.target.value }) } />
<label htmlFor="categoria">Categoría</label> <select id="categoria" value={filtros.categoria} onChange={(event) => dispatch({ type: 'cambiar-categoria', valor: event.target.value }) } > <option value="todas">Todas</option> <option value="hardware">Hardware</option> <option value="software">Software</option> </select>
<button type="button" onClick={() => dispatch({ type: 'alternar-favoritos' })} > Mostrar favoritos </button> </form> );}useReducer resulta útil cuando:
- Hay varias formas de actualizar el mismo estado.
- Las actualizaciones tienen nombres claros: añadir, eliminar, alternar, reiniciar.
- El estado tiene varias propiedades relacionadas.
- Quieres separar la lógica de actualización del JSX.
No es obligatorio usar useReducer en todos los formularios. Para uno o dos campos, useState suele ser suficiente.
6. useEffect como sincronización con sistemas externos
useEffect permite sincronizar un componente con algo que está fuera de React. Ese sistema externo puede ser una API, el almacenamiento del navegador, un temporizador, una suscripción, el título del documento o una biblioteca no controlada por React.
Ejemplo: sincronizar el título del documento con el número de resultados.
useEffect(() => { document.title = `${total} productos encontrados`;}, [total]);La estructura básica de un efecto es:
useEffect(() => { // Código de sincronización
return () => { // Limpieza opcional };}, [dependencias]);Las dependencias indican cuándo debe repetirse el efecto. Si el efecto usa total, userId o url, esos valores deben aparecer en el array de dependencias.
Ejemplo con temporizador y limpieza:
useEffect(() => { const id = setInterval(() => { console.log('Actualizando reloj'); }, 1000);
return () => { clearInterval(id); };}, []);La función de limpieza evita que el temporizador siga activo cuando el componente desaparece.
7. Cuándo no necesitas un efecto
Una parte importante de React avanzado consiste en no usar useEffect cuando no hace falta. Muchos efectos innecesarios aparecen porque se guarda como estado algo que podría calcularse directamente.
No es recomendable hacer esto:
function Lista({ productos, busqueda }) { const [filtrados, setFiltrados] = useState([]);
useEffect(() => { setFiltrados( productos.filter((producto) => producto.nombre.includes(busqueda)), ); }, [productos, busqueda]);
return <ul>{/* renderizado */}</ul>;}La lista filtrada es estado derivado. Puede calcularse sin efecto:
function Lista({ productos, busqueda }) { const filtrados = productos.filter((producto) => producto.nombre.toLowerCase().includes(busqueda.toLowerCase()), );
return ( <ul> {filtrados.map((producto) => ( <li key={producto.id}>{producto.nombre}</li> ))} </ul> );}Tampoco necesitas un efecto para responder a una acción del usuario si puedes hacerlo en el propio manejador del evento.
function FormularioPedido() { function manejarEnvio(event) { event.preventDefault(); // La acción ocurre porque el usuario envía el formulario. // No hace falta esperar a un useEffect. }
return <form onSubmit={manejarEnvio}>{/* campos */}</form>;}Preguntas útiles antes de usar useEffect:
- ¿Estoy sincronizando con algo externo a React?
- ¿Puedo calcular este valor a partir de props o estado?
- ¿Puedo ejecutar esta acción dentro de un evento?
- ¿Estoy creando una segunda fuente de verdad?
- ¿Necesito limpiar algo cuando el componente desaparezca?
8. Peticiones remotas con estados de carga y error
Cuando una pantalla consume una API, no basta con guardar los datos. También debes representar qué ocurre mientras se cargan, qué pasa si hay error y qué se muestra cuando la respuesta llega vacía.
Ejemplo básico con useEffect:
function ProductosRemotos() { const [productos, setProductos] = useState([]); const [cargando, setCargando] = useState(true); const [error, setError] = useState(null);
useEffect(() => { let ignorar = false;
async function cargarProductos() { try { setCargando(true); setError(null);
const respuesta = await fetch('/api/productos.json');
if (!respuesta.ok) { throw new Error('No se pudieron cargar los productos'); }
const datos = await respuesta.json();
if (!ignorar) { setProductos(datos); } } catch (error) { if (!ignorar) { setError(error.message); } } finally { if (!ignorar) { setCargando(false); } } }
cargarProductos();
return () => { ignorar = true; }; }, []);
if (cargando) { return <p>Cargando productos...</p>; }
if (error) { return <p role="alert">{error}</p>; }
if (productos.length === 0) { return <p>No hay productos disponibles.</p>; }
return <ListaProductos productos={productos} />;}La variable ignorar evita que una respuesta tardía intente modificar el estado cuando el componente ya no debe actualizarse. En proyectos reales también puedes usar AbortController para cancelar peticiones cuando sea adecuado.
Estados mínimos recomendables al consumir datos:
| Estado | Para qué sirve |
|---|---|
cargando | Mostrar que la petición está en curso. |
error | Informar de un fallo de red, servidor o formato. |
datos | Guardar la respuesta útil. |
| vacío | Mostrar un mensaje si la respuesta no contiene elementos. |
9. Hooks personalizados para reutilizar lógica
Un hook personalizado es una función que empieza por use y que puede usar otros hooks dentro. Sirve para extraer lógica reutilizable de un componente sin crear componentes artificiales.
Si varios componentes cargan datos de forma similar, puedes extraer esa lógica:
function useProductos() { const [productos, setProductos] = useState([]); const [cargando, setCargando] = useState(true); const [error, setError] = useState(null);
useEffect(() => { let ignorar = false;
async function cargar() { try { const respuesta = await fetch('/api/productos.json');
if (!respuesta.ok) { throw new Error('No se pudieron cargar los productos'); }
const datos = await respuesta.json();
if (!ignorar) { setProductos(datos); } } catch (error) { if (!ignorar) { setError(error.message); } } finally { if (!ignorar) { setCargando(false); } } }
cargar();
return () => { ignorar = true; }; }, []);
return { productos, cargando, error };}Uso desde un componente:
function CatalogoPage() { const { productos, cargando, error } = useProductos();
if (cargando) { return <p>Cargando...</p>; }
if (error) { return <p role="alert">{error}</p>; }
return <ListaProductos productos={productos} />;}Reglas prácticas:
- El nombre debe empezar por
use, comouseProductos,useFavoritosouseTema. - Debe representar una intención clara, no un conjunto arbitrario de instrucciones.
- Puede devolver datos, funciones o estados de carga.
- No debe usarse para ocultar lógica que solo se usa una vez y ya es clara en el componente.
10. Contexto para datos compartidos entre componentes lejanos
Las props son la forma normal de pasar datos de un componente padre a un hijo. Pero si un dato debe llegar a muchos niveles de profundidad, pasar props por componentes intermedios puede volverse incómodo.
El contexto permite que componentes lejanos lean un dato compartido sin pasarlo manualmente por cada nivel.
Ejemplo de contexto para favoritos:
const FavoritosContext = createContext(null);
function FavoritosProvider({ children }) { const [favoritos, setFavoritos] = useState([]);
function alternarFavorito(id) { setFavoritos((favoritosActuales) => favoritosActuales.includes(id) ? favoritosActuales.filter((favoritoId) => favoritoId !== id) : [...favoritosActuales, id], ); }
const value = { favoritos, alternarFavorito };
return ( <FavoritosContext.Provider value={value}> {children} </FavoritosContext.Provider> );}Hook para consumir el contexto:
function useFavoritos() { const contexto = useContext(FavoritosContext);
if (contexto === null) { throw new Error('useFavoritos debe usarse dentro de FavoritosProvider'); }
return contexto;}Uso en una tarjeta:
function ProductoCard({ producto }) { const { favoritos, alternarFavorito } = useFavoritos(); const esFavorito = favoritos.includes(producto.id);
return ( <article> <h2>{producto.nombre}</h2> <button onClick={() => alternarFavorito(producto.id)}> {esFavorito ? 'Quitar de favoritos' : 'Añadir a favoritos'} </button> </article> );}El contexto es útil para datos transversales: tema visual, idioma, usuario autenticado, preferencias o favoritos. No debe usarse para evitar pensar dónde debe vivir el estado. Si solo hay relación padre-hijo directa, las props suelen ser más claras.
11. Refs para acceder a valores o elementos externos al renderizado
useRef permite guardar un valor que permanece entre renderizados sin provocar un nuevo renderizado cuando cambia. También puede usarse para acceder a un elemento del DOM gestionado por React.
Ejemplo: enfocar un input al pulsar un botón.
function BuscadorConFoco() { const inputRef = useRef(null);
function enfocarBuscador() { inputRef.current.focus(); }
return ( <div> <label htmlFor="busqueda">Buscar producto</label> <input id="busqueda" ref={inputRef} /> <button type="button" onClick={enfocarBuscador}> Enfocar buscador </button> </div> );}Ejemplo: guardar un identificador de temporizador.
function Temporizador() { const intervaloRef = useRef(null);
function iniciar() { intervaloRef.current = setInterval(() => { console.log('tick'); }, 1000); }
function detener() { clearInterval(intervaloRef.current); }
return ( <div> <button onClick={iniciar}>Iniciar</button> <button onClick={detener}>Detener</button> </div> );}Usa refs para casos concretos:
- Enfocar un campo.
- Medir o acceder a un elemento del DOM.
- Guardar un identificador de temporizador.
- Mantener un valor que no se muestra en pantalla.
No uses refs para sustituir el estado cuando el valor debe mostrarse en la interfaz. Si un cambio debe actualizar la pantalla, usa estado.
12. Formularios con validación y mensajes accesibles
En React, un formulario avanzado debe controlar valores, validar entradas y comunicar errores de forma comprensible para cualquier usuario. La validación del cliente mejora la experiencia, pero no sustituye la validación del servidor.
Ejemplo con validación sencilla:
function FormularioProducto({ onCrear }) { const [nombre, setNombre] = useState(''); const [precio, setPrecio] = useState(''); const [errores, setErrores] = useState({});
function validar() { const nuevosErrores = {};
if (nombre.trim().length < 3) { nuevosErrores.nombre = 'El nombre debe tener al menos 3 caracteres.'; }
if (Number(precio) <= 0) { nuevosErrores.precio = 'El precio debe ser mayor que cero.'; }
return nuevosErrores; }
function manejarEnvio(event) { event.preventDefault();
const nuevosErrores = validar(); setErrores(nuevosErrores);
if (Object.keys(nuevosErrores).length > 0) { return; }
onCrear({ nombre: nombre.trim(), precio: Number(precio) }); setNombre(''); setPrecio(''); setErrores({}); }
return ( <form onSubmit={manejarEnvio} noValidate> <label htmlFor="nombre">Nombre</label> <input id="nombre" value={nombre} onChange={(event) => setNombre(event.target.value)} aria-describedby={errores.nombre ? 'error-nombre' : undefined} /> {errores.nombre && ( <p id="error-nombre" role="alert">{errores.nombre}</p> )}
<label htmlFor="precio">Precio</label> <input id="precio" type="number" min="0" step="0.01" value={precio} onChange={(event) => setPrecio(event.target.value)} aria-describedby={errores.precio ? 'error-precio' : undefined} /> {errores.precio && ( <p id="error-precio" role="alert">{errores.precio}</p> )}
<button type="submit">Crear producto</button> </form> );}Detalles importantes:
- Cada campo tiene
labelasociado conhtmlForeid. - Los mensajes de error se conectan al campo con
aria-describedby. role="alert"ayuda a anunciar cambios relevantes.- El formulario no recarga la página porque se usa
event.preventDefault(). - La validación se ejecuta al enviar, no en cada tecla si no aporta valor.
13. Organización de carpetas en una aplicación React
React no impone una estructura única. Para una aplicación educativa mediana, una organización sencilla puede ser suficiente:
src/ components/ ProductoCard.jsx ListaProductos.jsx PanelFiltros.jsx context/ FavoritosContext.jsx hooks/ useProductos.js useFavoritos.js pages/ CatalogoPage.jsx services/ productosService.js utils/ filtrarProductos.js App.jsx main.jsxResponsabilidades recomendadas:
| Carpeta | Contenido |
|---|---|
components | Componentes reutilizables o partes de una pantalla. |
pages | Componentes que representan pantallas completas. |
hooks | Hooks personalizados con lógica reutilizable. |
services | Funciones para comunicarse con APIs o fuentes de datos. |
context | Providers y hooks relacionados con contexto. |
utils | Funciones puras que no dependen de React. |
No necesitas crear todas estas carpetas desde el primer día. La estructura debe crecer con el proyecto. Si solo tienes tres componentes, una carpeta components puede bastar.
14. Introducción a rutas en aplicaciones cliente
Una aplicación React puede tener varias pantallas: catálogo, detalle, carrito, perfil o administración. En una SPA, la navegación suele gestionarse en el cliente mediante rutas.
Conceptualmente, una ruta conecta una URL con una pantalla:
| URL | Pantalla |
|---|---|
/ | Inicio |
/productos | Catálogo |
/productos/15 | Detalle del producto 15 |
/favoritos | Favoritos |
En esta unidad no añadiremos una librería de rutas para evitar desviar el foco. Lo importante es entender la idea: cada pantalla puede ser un componente de página, y la aplicación decide cuál mostrar según la ruta.
Ejemplo conceptual simplificado:
function App() { return ( <> <NavPrincipal /> <CatalogoPage /> </> );}Cuando estudies una librería de rutas, como React Router u otra solución del ecosistema, podrás conectar esas páginas a URLs reales. La base de componentes, estado, datos y formularios será la misma.
15. Accesibilidad en componentes interactivos
La accesibilidad no es una capa final. Debe formar parte del diseño del componente desde el principio. Una interfaz React puede ser visualmente correcta y, aun así, resultar difícil de usar con teclado o lector de pantalla.
Buenas prácticas básicas:
- Usa botones reales para acciones, no
divcononClick. - Asocia cada
inputcon unlabel. - Mantén un orden lógico de encabezados.
- No ocultes información solo mediante color.
- Indica estados de carga y error con texto.
- Usa
aria-describedbypara relacionar campos y mensajes. - Comprueba que los controles se pueden usar con teclado.
Ejemplo de botón accesible para favoritos:
function BotonFavorito({ esFavorito, onClick }) { return ( <button type="button" onClick={onClick} aria-pressed={esFavorito} > {esFavorito ? 'Quitar de favoritos' : 'Añadir a favoritos'} </button> );}aria-pressed comunica que el botón funciona como un interruptor. El texto visible también cambia, por lo que no depende solo del atributo ARIA.
16. Rendimiento sin sobreoptimizar
En React, el primer objetivo debe ser que el código sea correcto y claro. Optimizar antes de tener un problema real puede complicar el proyecto sin aportar mejora perceptible.
Medidas básicas antes de usar hooks de rendimiento:
- Mantén el estado lo más cerca posible de donde se usa.
- Evita guardar estado derivado innecesario.
- Divide componentes grandes cuando mezclen responsabilidades.
- Usa
keyestable en listas. - Evita cálculos pesados dentro del renderizado si se repiten mucho.
useDeferredValue puede ayudar cuando un valor cambia rápido, como un texto de búsqueda, y una parte de la interfaz puede actualizarse con menor prioridad.
function CatalogoConBusqueda({ productos }) { const [busqueda, setBusqueda] = useState(''); const busquedaDiferida = useDeferredValue(busqueda);
const productosFiltrados = filtrarProductos(productos, busquedaDiferida);
return ( <> <input value={busqueda} onChange={(event) => setBusqueda(event.target.value)} /> <ListaProductos productos={productosFiltrados} /> </> );}useTransition permite marcar una actualización como no urgente. Por ejemplo, escribir en un input debe sentirse inmediato, mientras que recalcular una lista grande puede ser menos prioritario.
function BuscadorAvanzado() { const [texto, setTexto] = useState(''); const [consulta, setConsulta] = useState(''); const [isPending, startTransition] = useTransition();
function manejarCambio(event) { const nuevoTexto = event.target.value; setTexto(nuevoTexto);
startTransition(() => { setConsulta(nuevoTexto); }); }
return ( <> <input value={texto} onChange={manejarCambio} /> {isPending && <p>Actualizando resultados...</p>} <Resultados consulta={consulta} /> </> );}No añadas useMemo o useCallback por costumbre. Son herramientas útiles cuando existe un cálculo caro, una referencia que provoca renderizados innecesarios o un componente optimizado que lo necesita.
17. TypeScript en componentes y hooks
TypeScript ayuda a documentar qué props recibe un componente, qué forma tienen los datos y qué valores pueden manejar los reducers. No cambia el modelo mental de React, pero reduce errores en aplicaciones medianas.
Ejemplo de props tipadas:
type Producto = { id: number; nombre: string; precio: number;};
type ProductoCardProps = { producto: Producto; onEliminar: (id: number) => void;};
function ProductoCard({ producto, onEliminar }: ProductoCardProps) { return ( <article> <h2>{producto.nombre}</h2> <p>{producto.precio} €</p> <button onClick={() => onEliminar(producto.id)}>Eliminar</button> </article> );}Ejemplo de acciones para reducer:
type Filtros = { busqueda: string; categoria: string;};
type AccionFiltros = | { type: 'cambiar-busqueda'; valor: string } | { type: 'cambiar-categoria'; valor: string } | { type: 'reiniciar' };Ventajas principales:
- Detecta props obligatorias no enviadas.
- Evita acceder a propiedades inexistentes.
- Documenta la forma de los datos de la API.
- Hace más seguros los reducers y hooks personalizados.
Si aún estás consolidando JavaScript, puedes practicar primero con .jsx y después migrar componentes concretos a .tsx.
18. Proyecto guiado: catálogo con filtros, favoritos y datos remotos
El proyecto de esta unidad será un catálogo de productos con carga de datos, filtros, favoritos y separación por responsabilidades. No usaremos backend real ni librerías externas: simularemos datos mediante un archivo JSON o una función de servicio.
Objetivos del proyecto:
- Cargar productos desde una fuente de datos.
- Mostrar estados de carga, error y lista vacía.
- Filtrar por búsqueda y categoría.
- Gestionar filtros con
useReducer. - Guardar favoritos en un contexto.
- Extraer la carga de datos a un hook personalizado.
- Mantener componentes con responsabilidades claras.
18.1. Definir la estructura del proyecto
Estructura propuesta:
src/ components/ EstadoCarga.jsx PanelFiltros.jsx ProductoCard.jsx ListaProductos.jsx context/ FavoritosContext.jsx hooks/ useProductos.js pages/ CatalogoPage.jsx services/ productosService.js utils/ filtrarProductos.js App.jsxEsta estructura separa el acceso a datos, la lógica reutilizable, el contexto y la representación visual.
18.2. Crear un servicio de datos
Un servicio encapsula cómo se obtienen los productos. Hoy puede leer un JSON local; mañana podría llamar a una API real.
export async function obtenerProductos() { const respuesta = await fetch('/productos.json');
if (!respuesta.ok) { throw new Error('No se pudieron obtener los productos'); }
return respuesta.json();}Separar esta función evita que los componentes conozcan detalles de la URL o del formato de petición.
18.3. Crear un hook para cargar productos
El hook useProductos concentra los estados de carga, error y datos.
import { useEffect, useState } from 'react';import { obtenerProductos } from '../services/productosService';
export function useProductos() { const [productos, setProductos] = useState([]); const [cargando, setCargando] = useState(true); const [error, setError] = useState(null);
useEffect(() => { let ignorar = false;
async function cargar() { try { setCargando(true); setError(null);
const datos = await obtenerProductos();
if (!ignorar) { setProductos(datos); } } catch (error) { if (!ignorar) { setError(error.message); } } finally { if (!ignorar) { setCargando(false); } } }
cargar();
return () => { ignorar = true; }; }, []);
return { productos, cargando, error };}El componente que use este hook no tendrá que repetir la lógica de petición.
18.4. Gestionar filtros con useReducer
Los filtros tienen varias acciones relacionadas, por eso useReducer encaja bien.
const filtrosIniciales = { busqueda: '', categoria: 'todas', soloFavoritos: false,};
function filtrosReducer(filtros, accion) { if (accion.type === 'busqueda') { return { ...filtros, busqueda: accion.valor }; }
if (accion.type === 'categoria') { return { ...filtros, categoria: accion.valor }; }
if (accion.type === 'favoritos') { return { ...filtros, soloFavoritos: !filtros.soloFavoritos }; }
if (accion.type === 'reiniciar') { return filtrosIniciales; }
return filtros;}Función pura para aplicar filtros:
export function filtrarProductos(productos, filtros, favoritos) { return productos.filter((producto) => { const coincideTexto = producto.nombre .toLowerCase() .includes(filtros.busqueda.toLowerCase());
const coincideCategoria = filtros.categoria === 'todas' || producto.categoria === filtros.categoria;
const coincideFavorito = !filtros.soloFavoritos || favoritos.includes(producto.id);
return coincideTexto && coincideCategoria && coincideFavorito; });}Como filtrarProductos no depende de React, puede probarse mentalmente como una función normal de JavaScript.
18.5. Compartir favoritos con contexto
El contexto permite que cualquier tarjeta consulte o cambie favoritos sin pasar props por toda la jerarquía.
import { createContext, useContext, useState } from 'react';
const FavoritosContext = createContext(null);
export function FavoritosProvider({ children }) { const [favoritos, setFavoritos] = useState([]);
function alternarFavorito(id) { setFavoritos((favoritosActuales) => favoritosActuales.includes(id) ? favoritosActuales.filter((favoritoId) => favoritoId !== id) : [...favoritosActuales, id], ); }
return ( <FavoritosContext.Provider value={{ favoritos, alternarFavorito }}> {children} </FavoritosContext.Provider> );}
export function useFavoritos() { const contexto = useContext(FavoritosContext);
if (contexto === null) { throw new Error('useFavoritos debe usarse dentro de FavoritosProvider'); }
return contexto;}En App se envuelve la pantalla:
function App() { return ( <FavoritosProvider> <CatalogoPage /> </FavoritosProvider> );}18.6. Componer la pantalla principal
La pantalla principal combina las piezas anteriores.
function CatalogoPage() { const { productos, cargando, error } = useProductos(); const { favoritos } = useFavoritos(); const [filtros, dispatch] = useReducer(filtrosReducer, filtrosIniciales);
const productosFiltrados = filtrarProductos(productos, filtros, favoritos);
return ( <main> <h1>Catálogo de productos</h1>
<PanelFiltros filtros={filtros} dispatch={dispatch} />
<EstadoCarga cargando={cargando} error={error} />
{!cargando && !error && ( <ListaProductos productos={productosFiltrados} /> )} </main> );}La lista delega cada tarjeta:
function ListaProductos({ productos }) { if (productos.length === 0) { return <p>No hay productos que coincidan con los filtros.</p>; }
return ( <section aria-label="Productos encontrados"> {productos.map((producto) => ( <ProductoCard key={producto.id} producto={producto} /> ))} </section> );}La tarjeta usa el contexto de favoritos:
function ProductoCard({ producto }) { const { favoritos, alternarFavorito } = useFavoritos(); const esFavorito = favoritos.includes(producto.id);
return ( <article> <h2>{producto.nombre}</h2> <p>{producto.precio} €</p> <p>{producto.categoria}</p>
<button type="button" aria-pressed={esFavorito} onClick={() => alternarFavorito(producto.id)} > {esFavorito ? 'Quitar de favoritos' : 'Añadir a favoritos'} </button> </article> );}El proyecto queda dividido en piezas pequeñas:
productosServiceobtiene los datos.useProductosgestiona carga y error.filtrosReducercentraliza la lógica de filtros.FavoritosProvidercomparte favoritos.CatalogoPagecompone la pantalla.ListaProductosyProductoCardse encargan de la presentación.
19. Criterios para elegir una solución
Cuando tengas dudas, usa estos criterios:
| Situación | Solución inicial razonable |
|---|---|
| Un dato solo lo usa un componente. | useState local. |
| Dos hijos necesitan el mismo dato. | Subir estado al padre común. |
| Un valor se calcula desde otros datos. | Estado derivado, sin useState adicional. |
| Hay muchas acciones sobre el mismo estado. | useReducer. |
| Varios componentes lejanos necesitan un dato. | Contexto, si pasar props complica el árbol. |
| Se repite lógica con hooks. | Hook personalizado. |
| Hay que sincronizar con API, temporizador o DOM externo. | useEffect con dependencias y limpieza. |
| Hay que enfocar un campo o guardar un valor no visual. | useRef. |
| La interfaz va lenta al escribir o filtrar. | Revisar estructura y considerar useDeferredValue o useTransition. |
La mejor arquitectura no es la más sofisticada, sino la que permite entender, cambiar y probar la aplicación con menos esfuerzo.
20. Ejercicios
Ejercicio 1: Separar componentes por responsabilidad
Toma un componente App que tenga buscador, lista, formulario y mensajes. Divídelo en al menos tres componentes con nombres claros y responsabilidad única.
Ejercicio 2: Identificar estado derivado
Revisa una lista filtrada guardada en estado. Reescríbela para calcularla directamente a partir de productos y busqueda durante el renderizado.
Ejercicio 3: Actualizar arrays sin mutación
Crea una lista de tareas y añade acciones para completar, eliminar y renombrar una tarea usando map, filter y copias de objeto.
Ejercicio 4: Reducer de filtros
Implementa un filtrosReducer con las acciones cambiar-busqueda, cambiar-categoria, alternar-favoritos y reiniciar.
Ejercicio 5: Efecto con limpieza
Crea un componente con un temporizador que muestre segundos transcurridos. Asegúrate de limpiar el intervalo cuando el componente desaparezca.
Ejercicio 6: Carga remota con estados
Carga una lista desde un JSON local o API de prueba. Muestra estados de carga, error, lista vacía y resultados.
Ejercicio 7: Hook personalizado
Extrae la lógica del ejercicio anterior a un hook llamado useDatos o useProductos, devolviendo datos, carga y error.
Ejercicio 8: Contexto de favoritos
Crea un contexto para favoritos y úsalo desde varias tarjetas de producto sin pasar favoritos por props intermedias.
Ejercicio 9: Formulario accesible
Construye un formulario de alta de producto con validación, label, mensajes relacionados mediante aria-describedby y mensajes de error visibles.
Ejercicio 10: Proyecto integrador
Construye el catálogo completo de la unidad: carga de productos, filtros con useReducer, favoritos con contexto, hook de datos, componentes separados y mensajes accesibles.