Ir al contenido
wannacode

Estado compartido

En resumen: El estado compartido en Vue viene de provide/inject o de un store de Pinia, 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ó.

Vue

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 Vue son provide e inject:

App.vue
<script setup>
const items = ref([]);
const count = computed(() => items.value.reduce((sum, i) => sum + i.qty, 0));
provide('cart', { items, count, add });
</script>
<!-- CartBadge.vue -->
<script setup>
const { count } = inject('cart');
</script>

provide('cart', …) da un valor bajo una clave a todos los descendientes del componente. inject('cart') lo toma del ancestro más cercano que lo proporcionó. Se pasan los refs (items, count), no sus valores, para que los consumidores sigan siendo reactivos.

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.

<script setup>
const items = ref([]);
const count = computed(() =>
items.value.reduce((s, i) => s + i.qty, 0));
function add(product) { /* … */ }
provide('cart', { items, count, add });
</script>
<template>
<Header />
<Catalog />
</template>
<!-- CartBadge.vue -->
<script setup>
const { count } = inject('cart');
</script>
<template>
<a href="/cart">Carrito ({{ count }})</a>
</template>
<!-- ProductCard.vue -->
<script setup>
const { add } = inject('cart');
</script>
<template>
<button @click="add(product)">Añadir</button>
</template>
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 llama a provide('cart', …). Cualquier descendiente de App puede leer el carrito.
  5. CartBadge y ProductCard lo leen con inject('cart'). 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.

Vue

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.

Para los stores Vue usa Pinia, la biblioteca oficial para el estado compartido. Un store se crea con defineStore y se escribe como <script setup>: un ref es el estado, un computed se deriva de él, las funciones son las acciones.

cart.js
export const useCartStore = defineStore('cart', () => {
const items = ref([]);
const count = computed(() => items.value.reduce((sum, i) => sum + i.qty, 0));
function add(product) { /* … */ }
return { items, count, add };
});

Pinia se instala una vez, en main.js: createApp(App).use(createPinia()).mount('#app'). Cada componente llama a useCartStore() y recibe el mismo store. No hace falta ningún provide en App.

<script setup>
import { storeToRefs } from 'pinia';
const cart = useCartStore();
const { count } = storeToRefs(cart);
</script>

El store es un objeto reactive: en la plantilla escribes cart.count, sin .value. Si quieres desarmarlo en variables, usa storeToRefs(cart). Da refs que siguen conectados al store. Las acciones (cart.add) se pueden tomar directamente.

Vue
Abre cart.js: ahí está toda la lógica del carrito. App.vue ya no tiene estado, y main.js instala Pinia.

Pruébalo

Encuentra el error

Las tarjetas cuentan, y el contador se queda en «Carrito (0)». No hay ningún error en la consola. ¿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 toma CartBadge el count del store para seguir conectado a él?
<script setup>
import { storeToRefs } from 'pinia';
import { useCartStore } from './cart.js';

const { count } = (useCartStore());
</script>
Más precisamente, para curiosos

Un store con opciones. defineStore también acepta un objeto: state: () => ({ items: [] }), getters y actions. Es lo mismo escrito como Options API. Pinia admite los dos estilos.

provide/inject o Pinia. provide viene incluido y sirve para lo que necesita un subárbol (los ajustes de una tabla, un formulario con campos anidados). Pinia es para el estado de toda la aplicación: se ve en las Vue DevTools, se prueba por separado y no depende de qué componente esté arriba.

Un símbolo como clave. En lugar de la cadena 'cart', provide suele usar un Symbol() exportado desde un archivo común. Así dos proveedores distintos no pueden tener la misma clave por casualidad.

inject con valor por defecto. inject('cart', null) no escribe ninguna advertencia cuando nadie proporcionó la clave, y devuelve null.

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.

Vue

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 dice const { count } = useCartStore();. ¿Qué muestra el contador?
3. ProductCard llama a inject('cart'), y nadie por encima ha llamado a provide('cart', …). ¿Qué pasa?
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 ·