Proyecto: carrito de compras
💡 🎯 Al terminar esta lección podrás…
- Planificar una aplicación de React dividiéndola en componentes.
- Combinar
useReducer, Context y efectos en un proyecto real. - Derivar valores en lugar de duplicarlos en estado.
- Persistir y recuperar datos del navegador.
1.Qué vas a construir
Vas a crear el carrito de compras de una tienda: un catálogo con buscador, un carrito donde se agregan, suman, restan y quitan productos, un total siempre correcto y el contenido guardado para que no se pierda al recargar. Es un proyecto pequeño, pero tiene la estructura de una aplicación real.
| Función de la app | Lo que aplicas | Lección |
|---|---|---|
| Dibujar el catálogo desde una lista de datos | Listas, key, props | 3 y 5 |
| Buscar productos mientras se escribe | Campos controlados, valores derivados | 5 y 6 |
| Agregar, sumar, restar y quitar productos | useReducer con acciones | 9 |
| Compartir el carrito entre componentes lejanos | Context y hook propio | 8 |
| Total y contador de artículos | Valores derivados con reduce | 5 |
| Guardar el carrito y recuperarlo | useEffect, localStorage, estado inicial perezoso | 7 |
| Mensaje de carrito vacío | Renderizado condicional | 5 |
💡 Consejo · Una sola fuente de verdad
El carrito vive en un solo lugar (el reductor del proveedor). El total y el número de artículos no se guardan: se calculan con reduce a partir de las líneas. Así nunca se desincronizan.
2.Plan: la arquitectura
Contextyy| Pieza | Responsabilidad |
|---|---|
reductor(lineas, accion) | Función pura con las acciones agregar, sumar, restar y quitar |
CarritoProvider | Guarda las líneas con useReducer, las persiste con un efecto y ofrece { lineas, dispatch, total, unidades } |
useCarrito() | Hook que lee el contexto |
Catalogo | Buscador + un <article className="producto"> por producto con su botón «Agregar» |
Carrito | Lista de líneas con «Restar», «Sumar» y «Quitar», más el total |
Cabecera | Muestra cuántos artículos hay en total |
El modelo de datos
// El carrito es un array de líneas:
[
{ id: 1, nombre: "Teclado", precio: 25, categoria: "Periféricos", cantidad: 2 },
{ id: 2, nombre: "Mouse", precio: 12.5, categoria: "Periféricos", cantidad: 1 },
]
// Y las acciones que lo cambian:
{ tipo: "agregar", producto } // suma 1 si ya existe, o crea la línea
{ tipo: "sumar", id }
{ tipo: "restar", id } // si llega a 0, la línea desaparece
{ tipo: "quitar", id }
3.La estructura que esperan las pruebas
Para que tu proyecto y el comprobador se entiendan, usa estos elementos y nombres:
| Elemento | Cómo debe ser |
|---|---|
| Cada producto del catálogo | <article className="producto"> con un <h3> (nombre), un <p className="precio"> (por ejemplo $25.00) y un botón «Agregar» |
| Buscador | <input type="search"> |
| Carrito | Un <aside aria-label="Carrito"> |
| Carrito vacío | Un <p className="vacio"> con «Tu carrito está vacío» |
| Cada línea | <li className="linea"> con <span className="cantidad"> y tres botones con aria-label: Restar, Sumar y Quitar |
| Total | <strong id="total"> con el formato $62.50 |
| Contador de artículos | <span id="contador"> con la suma de todas las cantidades |
| Persistencia | localStorage con la clave carrito (un JSON con las líneas) |
📌 Nota · Por qué aria-label
Los botones «−» y «+» no tienen texto que un lector de pantalla pueda leer con claridad. aria-label="Restar" y aria-label="Sumar" describen la acción; además es lo que usan las pruebas para encontrarlos.
4.Paso a paso
Paso 1 · El catálogo
Con PRODUCTOS.map(...) dibuja un <article className="producto"> por producto. Usa key={p.id}. Aún sin carrito: los botones pueden no hacer nada.
Paso 2 · El buscador
Un campo controlado (busqueda) y una lista derivada: PRODUCTOS.filter(p => p.nombre.toLowerCase().includes(busqueda.toLowerCase())). No guardes la lista filtrada en estado.
Paso 3 · El reductor
function reductor(lineas, accion) {
switch (accion.tipo) {
case "agregar": /* si existe: cantidad + 1; si no: nueva línea con cantidad 1 */
case "sumar": /* map: cantidad + 1 en la línea con ese id */
case "restar": /* map: cantidad - 1 y filter: descarta las que lleguen a 0 */
case "quitar": /* filter: descarta la línea con ese id */
default: return lineas;
}
}
Cada caso debe devolver un array nuevo; nunca modifiques el existente.
Paso 4 · Contexto y proveedor
Crea CarritoContext fuera de los componentes. En CarritoProvider usa useReducer(reductor, [], leerGuardado), calcula total y unidades con reduce y ofrécelos junto a lineas y dispatch. Añade el hook useCarrito().
Paso 5 · El carrito
Lee { lineas, dispatch, total }. Si lineas.length === 0 muestra el mensaje de vacío; si no, una <ul className="lineas"> con las líneas y sus tres botones. Muestra el total con moneda(total).
Paso 6 · Persistencia
Dentro del proveedor, un efecto con [lineas] guarda JSON.stringify(lineas) en localStorage. La función leerGuardado (tercer argumento de useReducer) lo recupera al iniciar, con try/catch por si el dato estuviera dañado.
💡 Consejo · Avanza probando
Después de cada paso, ejecuta el editor y comprueba que funciona antes de seguir. Un error aislado es fácil de encontrar; cinco errores mezclados, no.
5.Revisión de calidad
| Revisión | Cómo comprobarlo |
|---|---|
| Funciona | Agrega, suma, resta y quita; el total y el contador siempre coinciden |
| Sin avisos | La consola no muestra advertencias (por ejemplo, de key) |
| Sin mutaciones | Ningún push, splice ni asignación directa sobre el estado |
| Persiste | Recarga: el carrito sigue ahí |
| Accesible | Los botones sin texto tienen aria-label; el buscador tiene etiqueta |
| Legible | Componentes pequeños, nombres claros y sin lógica larga dentro del JSX |
6.Retos para ir más lejos
- Filtro por categoría: botones «Todas», «Periféricos» y «Pantallas».
- Cupón de descuento: un campo que aplique un 10 % si escribes
REACT10. - Envío gratis: muestra «Te faltan $X para el envío gratis» según el total.
- Ordenar el catálogo por precio o por nombre (recuerda copiar el array antes de
sort). - Productos desde una API: reemplaza
PRODUCTOSpor datos pedidos con el hookuseDatosde la lección 10. - Pasarlo a un proyecto de Vite, dividir los componentes en archivos y publicarlo en GitHub Pages (lección 11).
💡 Consejo · Añádelo a tu portafolio
Cuando lo tengas publicado, enlázalo desde tu página personal del curso de HTML. Una aplicación funcionando vale más que cualquier lista de tecnologías en un currículum.
7.Qué sigue en tu camino
Has recorrido el camino del frontend: HTML para la estructura, CSS para el diseño, JavaScript para la lógica y React para construir aplicaciones. Lo que viene es el backend: guardar los datos de verdad en un servidor y una base de datos.
| Paso | Qué aprenderás |
|---|---|
| TypeScript | Tipos para detectar errores antes de ejecutar el código |
| Node.js y APIs | Crear tu propio servidor y las rutas que consume React |
| Bases de datos | Guardar usuarios, productos y pedidos de forma permanente |
| Proyecto full stack | Esta tienda, con servidor, base de datos, usuarios y pagos |
📌 Nota · Próximamente
Los cursos de Node.js y APIs, Bases de datos y el proyecto full stack llegarán más adelante. Mientras tanto, aplica lo aprendido: construye cosas propias.
8.Errores frecuentes en el proyecto
- Mutar el estado en el reductor:
lineas.push(…)oexiste.cantidad++rompen la actualización. Crea copias conmapyfilter. - Guardar
totalen estado: se desincroniza. Calcúlalo conreduce. - Olvidar
.filter(l => l.cantidad > 0)al restar: quedan líneas con cantidad 0 o negativas. - Duplicar líneas: al agregar, comprueba primero si el producto ya está.
- Leer
localStorageen cada dibujo: usa el estado inicial perezoso (tercer argumento deuseReducer). - Olvidar la
keyen las listas de productos y de líneas. - Dar formato en varios sitios: usa una sola función
moneda(n). aria-labelausente en los botones «+» y «−».
📒 Apuntes para llevar
- El proyecto integra componentes, props, listas, estado, formularios, efectos, Context y reductores.
- Un reductor concentra todas las formas de cambiar el carrito; siempre devuelve un array nuevo.
- Context comparte
lineas,dispatch,totalyunidadessin pasar props. - Lo que se puede calcular (total, contador, lista filtrada) no se guarda en estado.
localStorage+useEffect+ estado inicial perezoso dan persistencia.- Usa nombres y estructura claros: facilitan el mantenimiento y las pruebas.
- El siguiente paso natural es el backend: Node.js, APIs y bases de datos.
9.Glosario
| Término | Significado |
|---|---|
| Reductor | Función pura que recibe el estado y una acción y devuelve el estado nuevo. |
| Acción | Objeto que describe un cambio, por ejemplo { tipo: "sumar", id: 1 }. |
| Proveedor | Componente que ofrece un contexto (y su estado) a todo su árbol. |
| Valor derivado | Dato calculado a partir del estado en cada dibujo. |
| Persistencia | Guardar datos para que sobrevivan a recargar la página. |
| Línea de pedido | Cada fila del carrito: un producto con su cantidad. |
🧠 Comprueba lo aprendido
Elige una respuesta en cada pregunta. Verás al instante si acertaste y por qué.
¿Por qué el total del carrito no se guarda en estado?
Los valores derivados se calculan en cada dibujo a partir de la fuente de verdad.
Al «restar» un producto con cantidad 1, ¿qué debe pasar?
El reductor descarta con filter las líneas cuya cantidad llega a 0.
¿Qué debe devolver siempre un caso del reductor?
Nunca se muta el estado anterior: se crean copias.
¿Cómo se recupera el carrito guardado al iniciar?
La función inicial se ejecuta solo una vez al montar.
¿Qué te permite el hook useCarrito()?
Encapsula useContext(CarritoContext) en un solo lugar.
Tu progreso se guarda en este navegador y se refleja en el índice del curso.
💡 🚀 ¡Ahora te toca!
La práctica fija lo aprendido. Pasa al ejercicio de esta lección.