Saltar al contenido
Curso de React
Todos los cursos Ir al ejercicio →

Proyecto: carrito de compras

  • ⏱ 150 minutos
  • 📶 Nivel: principiante
  • 🧩 Incluye ejercicio

💡 🎯 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 appLo que aplicasLección
Dibujar el catálogo desde una lista de datosListas, key, props3 y 5
Buscar productos mientras se escribeCampos controlados, valores derivados5 y 6
Agregar, sumar, restar y quitar productosuseReducer con acciones9
Compartir el carrito entre componentes lejanosContext y hook propio8
Total y contador de artículosValores derivados con reduce5
Guardar el carrito y recuperarlouseEffect, localStorage, estado inicial perezoso7
Mensaje de carrito vacíoRenderizado condicional5

💡 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

CarritoProviderreductor + localStorage
→Context
Cabeceracontador
→y
Catalogoagrega
→y
Carritomodifica y suma
PiezaResponsabilidad
reductor(lineas, accion)Función pura con las acciones agregar, sumar, restar y quitar
CarritoProviderGuarda las líneas con useReducer, las persiste con un efecto y ofrece { lineas, dispatch, total, unidades }
useCarrito()Hook que lee el contexto
CatalogoBuscador + un <article className="producto"> por producto con su botón «Agregar»
CarritoLista de líneas con «Restar», «Sumar» y «Quitar», más el total
CabeceraMuestra cuántos artículos hay en total

El modelo de datos

Cada línea del carrito
// 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:

ElementoCó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">
CarritoUn <aside aria-label="Carrito">
Carrito vacíoUn <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
PersistencialocalStorage 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

Esqueleto del 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ónCómo comprobarlo
FuncionaAgrega, suma, resta y quita; el total y el contador siempre coinciden
Sin avisosLa consola no muestra advertencias (por ejemplo, de key)
Sin mutacionesNingún push, splice ni asignación directa sobre el estado
PersisteRecarga: el carrito sigue ahí
AccesibleLos botones sin texto tienen aria-label; el buscador tiene etiqueta
LegibleComponentes pequeños, nombres claros y sin lógica larga dentro del JSX
⚛️ Una pista visual: el estilo de tu tienda (opcional)
Consola

6.Retos para ir más lejos

💡 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.

PasoQué aprenderás
TypeScriptTipos para detectar errores antes de ejecutar el código
Node.js y APIsCrear tu propio servidor y las rutas que consume React
Bases de datosGuardar usuarios, productos y pedidos de forma permanente
Proyecto full stackEsta 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

📒 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, total y unidades sin 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érminoSignificado
ReductorFunción pura que recibe el estado y una acción y devuelve el estado nuevo.
AcciónObjeto que describe un cambio, por ejemplo { tipo: "sumar", id: 1 }.
ProveedorComponente que ofrece un contexto (y su estado) a todo su árbol.
Valor derivadoDato calculado a partir del estado en cada dibujo.
PersistenciaGuardar datos para que sobrevivan a recargar la página.
Línea de pedidoCada 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?

Al «restar» un producto con cantidad 1, ¿qué debe pasar?

¿Qué debe devolver siempre un caso del reductor?

¿Cómo se recupera el carrito guardado al iniciar?

¿Qué te permite el hook useCarrito()?

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.

Ir al ejercicio