Saltearse al contenido

Comparte el estado entre Islas

Al construir tu proyecto usando la arquitectura de islas / hidratación parcial, puede que te hayas topado con este problema: Quiero compartir estado entre mis componentes.

Frameworks tales como React o Vue pueden alentar a usar proveedores de “contexto” (“context” providers) para que sea consumido por otros componentes. Pero al hidratar componentes parcialmente dentro de Astro o en Markdown, no puedes usar esos contextos envolventes.

Astro recomienda una solución diferente para el almacenamiento compartido en el lado del cliente: Nano Stores.

La librería Nano Stores te permite crear stores con las que cualquier componente puede interactuar. Recomendamos Nano Stores porque:

  • Son livianas. Nano Stores tiene la cantidad mínima de JS que puedas llegar a requerir (menos de 1 KB) con cero dependencias.
  • Son framework-agnósticas. ¡Esto significa que puedes compartir estado entre frameworks sin contratiempos! Astro está construido para ser flexible, así que amamos las soluciones que ofrecen una experiencia de desarrollo similar, sin importar tu preferencia.

Aun así, hay otras alternativas a explorar. Entre ellas puedes encontrar:

Para empezar, instala Nano Stores junto al paquete helper para tu framework favorito:

Ventana de terminal
npm install nanostores @nanostores/preact

¡Desde aquí, puedes saltar directamente a la guía de uso de Nano Stores, o seguir nuestra guía con ejemplos!

Ejemplo de uso - menú desplegable con carrito de ecommerce

Sección titulada Ejemplo de uso - menú desplegable con carrito de ecommerce

Digamos que queremos construir una interfaz de ecommerce simple con tres elementos interactivos:

  • Un formulario para “agregar al carrito”
  • Un menú desplegable con carrito para mostrar esos ítems agregados
  • Un botón para desplegar el menú con carrito

Prueba el ejemplo terminado en tu máquina u online vía StackBlitz.

Tu archivo base de Astro podría verse así:

src/pages/index.astro
---
import CartFlyoutToggle from '../components/CartFlyoutToggle';
import CartFlyout from '../components/CartFlyout';
import AddToCartForm from '../components/AddToCartForm';
---
<!DOCTYPE html>
<html lang="en">
<head>...</head>
<body>
<header>
<nav>
<a href="/">Tienda de Astro</a>
<CartFlyoutToggle client:load />
</nav>
</header>
<main>
<AddToCartForm client:load>
<!-- ... -->
</AddToCartForm>
</main>
<CartFlyout client:load />
</body>
</html>

Empecemos por abrir nuestro CartFlyout cada vez que cliqueamos en CartFlyoutToggle.

Primero, crea un nuevo archivo JS o TS para nuestro store. Usaremos un “atom” para esto:

src/cartStore.js
import { atom } from 'nanostores';
export const isCartOpen = atom(false);

Ahora, podemos importar este store dentro de cualquier archivo que necesite leer o escribir en ella. Comenzaremos conectando nuestro CartFlyoutToggle:

src/components/CartFlyoutToggle.jsx
import { useStore } from '@nanostores/preact';
import { isCartOpen } from '../cartStore';
export default function CartButton() {
// lee el valor del store con el hook `useStore`
const $isCartOpen = useStore(isCartOpen);
// escribe en el store importado usando `.set`
return (
<button onClick={() => isCartOpen.set(!$isCartOpen)}>Cart</button>
)
}

Luego, podemos leer el valor de isCartOpen en nuestro componente CartFlyout:

src/components/CartFlyout.jsx
import { useStore } from '@nanostores/preact';
import { isCartOpen } from '../cartStore';
export default function CartFlyout() {
const $isCartOpen = useStore(isCartOpen);
return $isCartOpen ? <aside>...</aside> : null;
}

Ahora, llevemos la cuenta de los ítems que hay dentro de tu carrito. Para evitar duplicados y llevar el registro de la “cantidad”, puedes guardar tu carrito como un objeto con el ID del ítem como key. Usaremos un Map para lograr esto.

Agreguemos un store cartItem a nuestro cartStore.js anterior. También puedes utilizar un archivo TypeScript si deseas definir el tipo de dato.

src/cartStore.js
import { atom, map } from 'nanostores';
export const isCartOpen = atom(false);
/**
* @typedef {Object} CartItem
* @property {string} id
* @property {string} name
* @property {string} imageSrc
* @property {number} quantity
*/
/** @type {import('nanostores').MapStore<Record<string, CartItem>>} */
export const cartItems = map({});

Ahora, exportemos una función helper addCartItem para que usen nuestros componentes.

  • Si el ítem no existe en el carrito, añade el carrito con una cantidad inicial de 1.
  • Si el ítem ya existe, aumenta la cantidad en 1.
src/cartStore.js
...
export function addCartItem({ id, name, imageSrc }) {
const existingEntry = cartItems.get()[id];
if (existingEntry) {
cartItems.setKey(id, {
...existingEntry,
quantity: existingEntry.quantity + 1,
})
} else {
cartItems.setKey(
id,
{ id, name, imageSrc, quantity: 1 }
);
}
}

Con la store en su lugar, ahora podemos llamar esta función dentro de AddToCartForm cada vez que el formulario es enviado. También desplegaremos el menú con carrito para poder ver un resumen de lo que hay dentro.

src/components/AddToCartForm.jsx
import { addCartItem, isCartOpen } from '../cartStore';
export default function AddToCartForm({ children }) {
// ¡usaremos valores fijos por simplicidad!
const hardcodedItemInfo = {
id: 'astronaut-figurine',
name: 'Astronaut Figurine',
imageSrc: '/images/astronaut-figurine.png',
}
function addToCart(e) {
e.preventDefault();
isCartOpen.set(true);
addCartItem(hardcodedItemInfo);
}
return (
<form onSubmit={addToCart}>
{children}
</form>
)
}

Finalmente, renderizamos los ítems dentro del carrito en nuestro componente CartFlyout:

src/components/CartFlyout.jsx
import { useStore } from '@nanostores/preact';
import { isCartOpen, cartItems } from '../cartStore';
export default function CartFlyout() {
const $isCartOpen = useStore(isCartOpen);
const $cartItems = useStore(cartItems);
return $isCartOpen ? (
<aside>
{Object.values($cartItems).length ? (
<ul>
{Object.values($cartItems).map(cartItem => (
<li>
<img src={cartItem.imageSrc} alt={cartItem.name} />
<h3>{cartItem.name}</h3>
<p>Cantidad: {cartItem.quantity}</p>
</li>
))}
</ul>
) : <p>¡Tu carrito está vacío!</p>}
</aside>
) : null;
}

Ya deberías tener un ejemplo de ecommerce totalmente interactivo con el menor paquete de JS de la galaxia 🚀

¡Prueba el ejemplo terminado en tu máquina u online vía StackBlitz!