Ir al contenido
wannacode

Estado compartido

En resumen: El estado compartido en React viene de un proveedor alto en el árbol con Context y useReducer, y los componentes lo leen directamente, sin props por cada nivel.

Problema

El contador vuelve a marcar 0

El carrito está elevado a App, como en la lección sobre elevar el estado. Desde ahí baja como props: a Header y desde él a CartBadge, a Catalog, desde él a ProductList y desde él a cada ProductCard. Alguien reescribió Header, y el contador se paró.

React

Añade pan, leche y otra vez pan. Las tarjetas cuentan, y la cabecera dice «Carrito (0)». App pasa count a Header, pero el nuevo Header no se lo pasa a CartBadge. Header no usa el carrito. Solo tenía que pasarlo, y justo eso olvidó.

La idea

Un proveedor arriba, consumidores abajo

Pasar datos por componentes que no los usan se llama prop drilling. Con dos niveles es normal. Con cinco, cada componente del camino depende del carrito, y una prop olvidada rompe la cadena.

Las dos bibliotecas tienen una forma de dar datos a descendientes lejanos sin los componentes del medio:

  1. El proveedor es el componente dueño de los datos. Dice: «todo lo que hay debajo de mí puede leer esto».
  2. El consumidor es el componente que los usa. Los toma directamente, sin importar cuántos niveles haya en medio.

En React esto es Context:

cart.js
export const CartContext = createContext(null);
// App.jsx
<CartContext value={{ items, count, add }}>
<Header />
<main><Catalog /></main>
</CartContext>
// CartBadge.jsx
const { count } = useContext(CartContext);

createContext(null) crea el «canal». null es el valor cuando no hay proveedor por encima del componente. <CartContext value={…}> es el proveedor. useContext(CartContext) lee el valor del proveedor más cercano hacia arriba en el árbol. Cuando el valor cambia, cada componente que lo lee se dibuja de nuevo.

Los datos siguen teniendo un dueño, y los cambios siguen pasando por él: add es la función del proveedor. Solo cambió el camino.

Míralo

Mira las dos formas

Al principio el carrito baja nivel por nivel, y la cadena se rompe por la mitad. Después App se convierte en proveedor y los dos consumidores leen directamente.

const CartContext = createContext(null);
function App() {
const [items, setItems] = useState([]);
const count = items.reduce((s, i) => s + i.qty, 0);
function add(product) { /* … */ }
return (
<CartContext value={{ items, count, add }}>
<Header />
<Catalog />
</CartContext>
);
}
function Header() {
return <header><CartBadge /></header>;
}
function CartBadge() {
const { count } = useContext(CartContext);
return <a href="/cart">Carrito ({count})</a>;
}
function ProductCard({ product }) {
const { add } = useContext(CartContext);
return <button onClick={() => add(product)}>Añadir</button>;
}
Paso a paso, en palabras
  1. El carrito vive en App. Dos componentes muy abajo lo necesitan: CartBadge en la cabecera y ProductCard en el catálogo.
  2. Solo con props, el carrito baja nivel por nivel. Header, Catalog y ProductList no lo usan. Solo lo pasan.
  3. Alguien reescribe Header y olvida count. La cadena se rompe por la mitad y el contador muestra 0. Eso es el prop drilling.
  4. En su lugar: App envuelve el árbol en <CartContext value={…}>. Todo lo que hay debajo puede leer el carrito.
  5. CartBadge y ProductCard lo leen con useContext(CartContext). Los componentes intermedios no saben nada del carrito.
  6. Un clic en «Añadir» llama a add(product). La función vino del proveedor, por eso el cambio ocurre en App.
  7. El carrito de App cambia y cada componente que lo lee se dibuja de nuevo: el contador dice 1, la tarjeta dice 1.
  8. Los datos siguen teniendo un solo dueño, App. Solo cambió el camino: no por cada nivel, sino directo desde el proveedor.

Los componentes del medio ya no saben nada del carrito. El nuevo Header lo puedes reescribir tantas veces como quieras.

Pruébalo

Quita el drilling

El carrito baja como props por Header y Catalog, y Header pierde count. Da el carrito desde arriba. Header, Catalog y ProductList no reciben nada sobre el carrito, y CartBadge y ProductCard lo leen directamente. La comprobación añade pan, leche y otra vez pan y mira las tarjetas y el contador.

React

La idea

Un store para el carrito

El carrito crece: añadir, quitar, cambiar la cantidad, vaciarlo. Si cada acción es una función en App, App se llena de lógica que no le corresponde. Por eso el estado compartido se muda a un archivo aparte, un store: los datos y todo lo que los cambia, en un solo lugar.

React tiene para esto el hook incorporado useReducer. En lugar de setItems(…), los componentes envían una acción (dispatch({ type: 'add', product })), y una sola función, el reducer, decide qué pasa con el carrito:

function cartReducer(items, action) {
switch (action.type) {
case 'add': /* un array nuevo con el producto */
default:
return items;
}
}
export function CartProvider({ children }) {
const [items, dispatch] = useReducer(cartReducer, []);
const add = (product) => dispatch({ type: 'add', product });
return <CartContext value={{ items, count, add }}>{children}</CartContext>;
}

CartProvider es el proveedor, y App solo lo pone alrededor del árbol. Los consumidores no cambian: siguen leyendo useContext(CartContext). El reducer es una función corriente: entran el carrito viejo y una acción, sale un carrito nuevo. No toca nada más, así que es fácil de probar.

Context y useReducer vienen incluidos en React. También hay bibliotecas para stores, por ejemplo Zustand y Redux Toolkit. No están en el sandbox, y aquí solo las mencionamos: la idea es la misma, una fuente y acciones que la cambian.

React
Abre cart.jsx: ahí está toda la lógica del carrito. CartBadge y ProductCard son los mismos que en la tarea.

Pruébalo

Encuentra el error

La página falla en cuanto se abre: TypeError: Cannot destructure property 'count' of … as it is null. ¿Qué línea tiene la culpa?

Elige la línea con el error.

La idea

Cuándo compartir y cuándo no

El estado compartido es cómodo, y por eso es tentador meterlo todo ahí. Pero todo lo que hay en él se puede leer y cambiar desde cualquier sitio, y eso hace los errores más difíciles de encontrar. Sigue valiendo la regla de la lección sobre elevar: tan alto como haga falta, y no más.

  • Compartido: lo que necesitan partes lejanas del árbol y lo que vive mientras el usuario está en el sitio. El carrito, el usuario con sesión iniciada, el idioma, el tema.
  • En el componente: los campos de un formulario, si el menú está abierto, qué pestaña está elegida, el texto del cuadro de búsqueda. Lo necesita un componente (o su padre) y se va con él.
  • Los datos del servidor no son estado compartido. La lista de productos y los pedidos tienen un dueño, y el dueño es el servidor. En un store son una copia que hay que refrescar. El componente que los muestra los carga, como en la lección sobre datos de una API.

El estado compartido no cambia el sentido: los datos parten de un dueño, y los cambios pasan por sus funciones.

Pruébalo

Rellena el hueco

¿Cómo lee CartBadge la cuenta del proveedor?
import { useContext } from 'react';
import { CartContext } from './cart.js';

export default function CartBadge() {
  const { count } = (CartContext);
  return <a href="/cart">Carrito ({count})</a>;
}
Más precisamente, para curiosos

<CartContext.Provider>. Antes de React 19 el proveedor se escribía <CartContext.Provider value={…}>. Esa forma sigue funcionando, y <CartContext value={…}> es la nueva, más corta.

Tu propio hook. Junto a un contexto suele haber una pequeña función export function useCart() { return useContext(CartContext); }. También puede lanzar un error claro cuando no hay proveedor, en lugar de un TypeError sobre null.

Quién se dibuja de nuevo. Cada componente que lee el contexto se dibuja de nuevo cuando cambia su valor. value={{ items, count, add }} es un objeto nuevo en cada dibujado del proveedor. En una aplicación grande esto se protege con useMemo o con varios contextos más pequeños. En la lección sobre velocidad verás cómo medirlo.

Los componentes de clase leen un contexto con static contextType o con <CartContext.Consumer>. Los encontrarás en código antiguo.

El store vive mientras la página está abierta. Al recargar se pierde. Un carrito que debe sobrevivir se guarda en localStorage o en el servidor.

Las comprobaciones ven el comportamiento. Las tareas de aquí no ven si el carrito viene de props, de un contexto o de un store. Comprueban que el contador y las tarjetas muestren lo mismo.

Ejercicio

La página del carrito

En la parte principal está CartSummary: un encabezado El carrito, filas Pan blanco × 2 con un botón Quitar, Total: 4,00 €, y El carrito está vacío cuando está vacío. Ahora tiene su propio carrito que nadie rellena. Añade al store el quitar y un total, y haz que CartSummary lea el carrito de ahí. La comprobación añade tres productos, los quita y mira el contador, las tarjetas y el total.

React

Test

Ponte a prueba

1. El carrito baja desde App por Catalog y ProductList hasta ProductCard. Catalog y ProductList no lo usan. ¿Cuál es el problema?
2. CartBadge llama a useContext(CartContext), pero está fuera de <CartContext value={…}>. ¿Qué recibe?
3. ¿Por qué cartReducer devuelve un array nuevo en lugar de hacer items.push(…)?
4. ¿Cuál de estos pertenece al estado compartido?
5. La lista de productos viene de /api/products. ¿Debe ir al store compartido?

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