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:
FAQ
🙋 ¿Puedo usar Nano Stores en archivos .astro
u otros archivos del lado del servidor? Las Nano Stores pueden ser importadas, escritas y leídas desde componentes del lado del servidor, ¡aunque no lo recomendamos! . Esto se debe a ciertas restricciones:
Escribir en un store desde un archivo .astro
o un componente no-hidratado no afectará el valor recibido por un componente del lado del cliente .
No puedes pasar una Nano Store como “prop” a componentes del lado del cliente.
No puedes suscribirte a cambios en la store desde un archivo .astro
, ya que los componentes de Astro no se re-renderizan.
¡Si entiendes estas restricciones y aun así encuentras un caso de uso, puedes darle una oportunidad a Nano Stores! Solamente recuerda que Nano Stores fue creado específicamente para reaccionar a cambios en el cliente .
🙋 ¿Cómo se comparan las Svelte stores a Nano Stores? ¡Nano Stores y Svelte stores son muy similares! De hecho, nanostores te permite usar el mismo atajo $
para suscripciones que puedes utilizar con las Svelte stores.
Si quieres evitar usar una librería de terceros, Svelte stores es una gran herramienta para la comunicación entre islas. Aun así, puedes llegar a preferir Nano Stores si a) te gustaría añadir add-ons para “objetos” y estado asíncrono , o b) quieres comunicarte entre Svelte y otros frameworks como Preact o Vue.
🙋 ¿Cómo se comparan las Solid signals a Nano Stores? Si has usado Solid anteriormente, habrás intentado mover signals o stores fuera de tus componentes. ¡Esta es una muy buena manera de compartir estado entre islas de Solid! Intenta exportar signals desde un archivo compartido:
import { createSignal } from ' solid-js ' ;
export const sharedCount = createSignal ( 0 );
…y todos los componentes que importen sharedCount
compartirán el mismo estado. Aunque esto funciones bien, puedes llegar a preferir Nano Stores si a) te gustaría añadir add-ons para “objetos” y estado asíncrono , o b) quieres comunicarte entre Solid y otros frameworks como Preact or Vue.
Para empezar, instala Nano Stores junto al paquete helper para tu framework favorito:
npm install nanostores @nanostores/preact
npm install nanostores @nanostores/react
npm install nanostores @nanostores/solid
npm install nanostores @nanostores/vue
npm install nanostores @nanostores/lit
¡Desde aquí, puedes saltar directamente a la guía de uso de Nano Stores , o seguir nuestra guía con ejemplos!
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í:
---
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:
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
:
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 >
)
}
import { useStore } from ' @nanostores/react ' ;
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 >
)
}
import { useStore } from ' @nanostores/solid ' ;
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 >
)
}
< script >
import { isCartOpen } from ' ../cartStore ' ;
</ script >
<!--usa "$" para leer el valor del store-->
< button on :click= { () => isCartOpen . set ( ! $ isCartOpen) } > Cart </ button >
< template >
<!--escribe en el store importado usando `.set`-->
< button @click = " isCartOpen.set(!$isCartOpen) " > Cart </ button >
</ template >
< script setup >
import { isCartOpen } from ' ../cartStore ' ;
import { useStore } from ' @nanostores/vue ' ;
// lee el valor del store con el hook `useStore`
const $isCartOpen = useStore ( isCartOpen );
</ script >
import { LitElement, html } from ' lit ' ;
import { isCartOpen } from ' ../cartStore ' ;
export class CartFlyoutToggle extends LitElement {
handleClick () {
isCartOpen . set ( ! isCartOpen . get ());
}
render () {
return html `
<button @click=" ${ this . handleClick } ">Cart</button>
` ;
}
}
customElements . define ( ' cart-flyout-toggle ' , CartFlyoutToggle);
Luego, podemos leer el valor de isCartOpen
en nuestro componente CartFlyout
:
import { useStore } from ' @nanostores/preact ' ;
import { isCartOpen } from ' ../cartStore ' ;
export default function CartFlyout () {
const $isCartOpen = useStore ( isCartOpen );
return $isCartOpen ? < aside > ... </ aside > : null ;
}
import { useStore } from ' @nanostores/react ' ;
import { isCartOpen } from ' ../cartStore ' ;
export default function CartFlyout () {
const $isCartOpen = useStore ( isCartOpen );
return $isCartOpen ? < aside > ... </ aside > : null ;
}
import { useStore } from ' @nanostores/solid ' ;
import { isCartOpen } from ' ../cartStore ' ;
export default function CartFlyout () {
const $isCartOpen = useStore ( isCartOpen );
return $isCartOpen () ? < aside > ... </ aside > : null ;
}
< script >
import { isCartOpen } from ' ../cartStore ' ;
</ script >
{# if $isCartOpen}
< aside > ... </ aside >
{/ if }
< template >
< aside v-if = " $isCartOpen " > ... </ aside >
</ template >
< script setup >
import { isCartOpen } from ' ../cartStore ' ;
import { useStore } from ' @nanostores/vue ' ;
const $isCartOpen = useStore ( isCartOpen );
</ script >
import { isCartOpen } from ' ../cartStore ' ;
import { LitElement, html } from ' lit ' ;
import { StoreController } from ' @nanostores/lit ' ;
export class CartFlyout extends LitElement {
private cartOpen = new StoreController ( this , isCartOpen);
render () {
return this . cartOpen . value ? html ` <aside>...</aside> ` : null ;
}
}
customElements . define ( ' cart-flyout ' , CartFlyout);
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.
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 ( {} );
import { atom, map } from ' nanostores ' ;
export const isCartOpen = atom ( false );
export type CartItem = {
id : string ;
name : string ;
imageSrc : string ;
quantity : number ;
}
export const cartItems = map < Record < string , CartItem >> ( {} );
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.
...
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 }
);
}
}
...
type ItemDisplayInfo = Pick < CartItem , ' id ' | ' name ' | ' imageSrc ' >;
export function addCartItem ( { id , name , imageSrc } : ItemDisplayInfo ) {
const existingEntry = cartItems . get ()[id];
if (existingEntry) {
cartItems . setKey (id , {
... existingEntry ,
quantity: existingEntry . quantity + 1 ,
});
} else {
cartItems . setKey (
id ,
{ id , name , imageSrc , quantity: 1 }
);
}
}
Nota
🙋 ¿Por qué usamos .get()
aquí en vez de un helper useStore
? Habrás notado que estamos llamando a cartItems.get()
aquí, en vez de usar el helper useStore
de nuestros ejemplos de React / Preact / Solid / Vue. Esto es porque useStore genera re-renderizados. En otras palabras, useStore
debe usarse cada vez que el valor del store se renderice en la UI. Como estamos leyendo este valor cuando un evento es accionado (addToCart
en este caso), y no estamos intentando renderizar ese valor, en este caso no necesitamos useStore
.
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.
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 >
)
}
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 >
)
}
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 >
)
}
< form on :submit| preventDefault = { addToCart } >
< slot ></ slot >
</ form >
< script >
import { addCartItem, isCartOpen } from ' ../cartStore ' ;
// ¡usaremos valores fijos por simplicidad!
const hardcodedItemInfo = {
id: ' astronaut-figurine ' ,
name: ' Astronaut Figurine ' ,
imageSrc: ' /images/astronaut-figurine.png ' ,
}
function addToCart () {
isCartOpen . set ( true );
addCartItem ( hardcodedItemInfo );
}
</ script >
< template >
< form @submit = " addToCart " >
< slot ></ slot >
</ form >
</ template >
< script setup >
import { addCartItem, isCartOpen } from ' ../cartStore ' ;
// ¡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 );
}
</ script >
import { LitElement, html } from ' lit ' ;
import { isCartOpen, addCartItem } from ' ../cartStore ' ;
export class AddToCartForm extends LitElement {
static get properties () {
return {
item: { type: Object },
};
}
constructor () {
super ();
this . item = {};
}
addToCart ( e ) {
e . preventDefault ();
isCartOpen . set ( true );
addCartItem ( this . item );
}
render () {
return html `
<form @submit=" ${ this . addToCart } ">
<slot></slot>
</form>
` ;
}
}
customElements . define ( ' add-to-cart-form ' , AddToCartForm);
Finalmente, renderizamos los ítems dentro del carrito en nuestro componente CartFlyout
:
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 ;
}
import { useStore } from ' @nanostores/react ' ;
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 ;
}
import { useStore } from ' @nanostores/solid ' ;
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 ;
}
< script >
import { isCartOpen, cartItems } from ' ../cartStore ' ;
</ script >
{# if $isCartOpen}
{# if Object . values ($cartItems) . length }
< aside >
{# each Object . values ($cartItems) as cartItem}
< li >
< img src = { cartItem . imageSrc } alt = { cartItem . name } />
< h3 > { cartItem . name } </ h3 >
< p > Cantidad: { cartItem . quantity } </ p >
</ li >
{/ each }
</ aside >
{# else }
< p > ¡Tu carrito está vacío! </ p >
{/ if }
{/ if }
< template >
< aside v-if = " $isCartOpen " >
< ul v-if = " Object.values($cartItems).length " >
< li v-for = " cartItem in Object.values($cartItems) " v-bind:key = " cartItem.name " >
< img :src = cartItem.imageSrc :alt = cartItem.name />
< h3 > {{cartItem.name}} </ h3 >
< p > Cantidad: {{cartItem.quantity}} </ p >
</ li >
</ ul >
< p v-else > ¡Tu carrito está vacío! </ p >
</ aside >
</ template >
< script setup >
import { cartItems, isCartOpen } from ' ../cartStore ' ;
import { useStore } from ' @nanostores/vue ' ;
const $isCartOpen = useStore ( isCartOpen );
const $cartItems = useStore ( cartItems );
</ script >
import { LitElement, html } from ' lit ' ;
import { isCartOpen, cartItems } from ' ../cartStore ' ;
import { StoreController } from ' @nanostores/lit ' ;
export class CartFlyoutLit extends LitElement {
private cartOpen = new StoreController ( this , isCartOpen);
private getCartItems = new StoreController ( this , cartItems);
renderCartItem ( cartItem ) {
return html `
<li>
<img src=" ${ cartItem . imageSrc } " alt=" ${ cartItem . name } " />
<h3> ${ cartItem . name } </h3>
<p>Cantidad: ${ cartItem . quantity } </p>
</li>
` ;
}
render () {
return this . cartOpen . value
? html `
<aside>
${
Object . values ( this . getCartItems . value ) . length
? html `
<ul>
${ Object . values ( this . getCartItems . value ) . map ( ( cartItem ) =>
this . renderCartItem (cartItem)
) }
</ul>
`
: html ` <p>¡Su carro está vacío!</p> `
}
</aside>
`
: null ;
}
}
customElements . define ( ' cart-flyout ' , CartFlyoutLit);
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!
Learn