Ir al contenido
wannacode

Estado compartido: hacia arriba

En resumen: Subir el estado (lifting state up) lleva el estado compartido al padre común más cercano: los datos bajan como props y los cambios suben mediante un callback.

Problema

El contador de arriba no se entera

La tienda tiene arriba un contador del carrito y debajo una lista de productos. Añade pan, leche y otro pan.

React

La tarjeta del pan dice «En el carrito: 2», y arriba pone «Carrito (0)». El carrito vive en ProductList: ahí se pulsa el botón y ahí se puso el estado. CartBadge es otro componente, hermano de ProductList, y tiene su propio número que nadie cambia. Los dos componentes tienen dos carritos, y cada uno tiene razón con el suyo.

La idea

Hasta el padre común

Un componente ve solo su propio estado y las props que le pasa su padre. Un hermano no ve a su hermano. Así que dos sitios que muestran un carrito no pueden tener cada uno el suyo.

La solución es llevar el estado al padre común más cercano de los dos. Aquí es App. Esto se llama subir el estado (lifting state up):

  1. App guarda items y calcula count.
  2. Los datos bajan como props: count a CartBadge, items a ProductList.
  3. Los cambios suben como mensaje: el botón de ProductCard no cambia el carrito, le dice a App: «añade este producto».
export default function App() {
const [items, setItems] = useState([]);
const count = items.reduce((sum, i) => sum + i.qty, 0);
function add(product) { /* cambia items */ }
return (
<>
<header><CartBadge count={count} /></header>
<main><ProductList products={PRODUCTS} items={items} onAdd={add} /></main>
</>
);
}

onAdd={add} pasa la función hacia abajo. El hijo la llama cuando pasa algo: onClick={() => onAdd(product)}. Una función así se llama callback.

Ahora CartBadge y ProductList ya no tienen estado propio para el carrito. Muestran lo que les da App.

Míralo

Mira cómo sube

Al principio el carrito está en ProductList. Después sube a App y fluye desde allí hacia abajo.

function App() {
const [items, setItems] = useState([]);
const count = items.reduce((s, i) => s + i.qty, 0);
function add(product) {
setItems(plusOne(items, product));
}
return (
<>
<CartBadge count={count} />
<ProductList items={items} onAdd={add} />
</>
);
}
function ProductList({ items, onAdd }) {
return PRODUCTS.map((p) => (
<ProductCard key={p.id} product={p}
qty={qtyOf(items, p)} onAdd={onAdd} />
));
}
function ProductCard({ product, qty, onAdd }) {
return <button onClick={() => onAdd(product)}>…</button>;
}
Paso a paso, en palabras
  1. CartBadge y ProductList son hermanos: los dos son hijos de App. Al principio el carrito vive en ProductList, y CartBadge tiene su propio contador.
  2. Un clic en «Añadir» llega a ProductList y la lista cuenta 1. CartBadge no ve ese estado: un hermano no lee el estado de su hermano. Se queda en 0.
  3. La solución: llevar el carrito al padre común más cercano de los dos, App. El contador deja su propia copia.
  4. Desde App los datos bajan como props: count a CartBadge, items a ProductList y qty a cada ProductCard. Los dos hermanos leen ahora un solo carrito.
  5. El hijo no es dueño del carrito, así que no puede cambiarlo. El clic llama a onAdd(product): una función que App pasó hacia abajo. La llamada sube hasta add() en App.
  6. App cambia items y las props nuevas bajan: el contador dice 2, la tarjeta dice 2. Nadie los actualizó a mano.
  7. La regla: el estado vive en un solo sitio, tan arriba como haga falta y no más. Los datos bajan como props, los cambios suben como eventos.

La dirección es la misma en cualquier app: datos hacia abajo, mensajes hacia arriba. Quien tiene los datos es el único que los cambia.

Pruébalo

Sube el carrito

El carrito está en ProductList, y CartBadge cuenta un número propio. Sube el carrito a App: CartBadge recibe la cantidad como prop, ProductList recibe items y avisa «Añadir» hacia arriba. La comprobación añade pan, leche y pan y mira las tarjetas y el contador.

React

La idea

El hijo avisa, el padre decide

ProductCard no sabe cómo está construido el carrito. Solo sabe que alguien pulsó «Añadir» en su producto. Así que avisa de lo que pasó, no de lo que hay que cambiar: «añade este producto», no «sube qty en la línea 2». App decide qué significa eso: una línea nueva o una unidad más.

Esto deja la tarjeta más simple, y puede estar en cualquier sitio: en el catálogo, en «Recomendados», en la búsqueda. No es dueña de los datos y no los toca.

Los nombres siguen una convención: una prop con una función empieza por on y nombra el evento: onAdd, onRemove, onChange. La función del padre suele llamarse add o handleAdd. Pasas la función, no su resultado: onClick={() => onAdd(product)}, no onClick={onAdd(product)}.

Pruébalo

Rellena el hueco

¿Qué va en el hueco para que el clic llegue al padre?

export default function ProductCard({ product, onAdd }) {
  return <button onClick={() => (product)}>Añadir</button>;
}

Pruébalo

Encuentra el error

La página se abre y el contador del carrito crece solo, sin ningún clic. ¿Qué línea tiene la culpa?

Elige la línea con el error.

La idea

Cuando el árbol se hace profundo

¿Hasta dónde hay que subirlo? Tan arriba como haga falta, y no más. Si solo ProductCard usa algo (si la descripción está abierta), se queda en ella. Lo subes cuando un segundo componente tiene que verlo.

En una tienda real el carrito hace falta en muchos sitios: el contador de arriba, la lista, la página del producto, el pedido. El padre común está muy arriba, y el camino hasta ellos pasa por componentes que no usan el carrito, solo lo pasan hacia abajo. Esto se llama prop drilling. Con dos niveles es normal. Con cinco niveles, cada componente del camino tiene que saber de datos que no son suyos.

Para esto hay herramientas aparte: Context. Entregan los datos a descendientes lejanos sin pasar por los componentes del camino. Están en la lección sobre estado compartido del módulo «Efectos, datos y formularios». La regla sigue igual: los datos tienen un dueño, y los cambios pasan por él.

Más precisamente, para curiosos

Las props son de solo lectura. En modo de desarrollo, React congela el objeto de props: props.qty = 3 lanza TypeError: Cannot assign to read only property 'qty'. Un hijo que quiere un cambio avisa hacia arriba.

Por qué una flecha. onClick quiere una función que React llame más tarde. onClick={add} también es una función, pero React la llama con el evento del clic, no con el producto. Por eso onClick={() => onAdd(product)}: una función nueva que conoce el producto.

Los componentes de clase tienen el mismo modelo: el padre pasa onAdd como prop y el hijo llama a this.props.onAdd(product).

Una copia no es subir el estado. Si el padre guarda el carrito y el hijo hace su propia copia con useState(items), la copia toma solo el primer valor y deja de seguir al padre. Es el error de la lección sobre estado derivado.

Ejercicio

El carrito en el lateral

Junto a la lista hay una tercera parte, CartSummary: el encabezado «Tu carrito», líneas como «Pan blanco × 2» con un botón «Quitar», «Total: 4,00 €» y «El carrito está vacío» cuando está vacío. Ahora mismo CartSummary guarda un carrito propio que nadie llena. Dale el carrito de App y haz que «Quitar» quite de él. La comprobación añade tres productos, los quita y mira el contador, las tarjetas y el total.

React

Test

Ponte a prueba

1. CartBadge está en la cabecera, ProductList está en la parte principal. Los dos son hijos de App. ¿Dónde debe estar el carrito?
2. ProductCard dice onClick={onAdd(product)}. ¿Qué pasa?
3. ¿Por qué ProductCard no cambia el carrito ella misma, sino que avisa hacia arriba?
4. El carrito lo necesita un componente cinco niveles por debajo de App. Los tres componentes del camino solo lo pasan hacia abajo. ¿Cómo se llama esto?

Creado por Ivo Gergov como parte de la campaña de Evo Tech por una educación accesible en programación ·