Ao construir um website Astro com a arquitetura em ilhas / hidratação parcial , você pode ter esse problema: Eu quero compartilhar estado entre meus componentes.
Frameworks de UI como React ou Vue podem encorajar provedores de “contexto” para outros componentes consumirem. Porém, ao parcialmente hidratar componentes no Astro ou Markdown, você não pode utilizar esses invólucros de contexto.
Astro recomenda uma solução diferente para armazenamento compartilhado no lado do cliente: Nano Stores .
Por que Nano Stores?
A biblioteca Nano Stores permite que você escreva stores que qualquer componente pode interagir com. Nós recomendamos Nano Stores pois:
São leves. Nano Stores envia o mínimo de JS que você vai precisar (menos do que 1KB) com zero dependências.
São agnósticos a frameworks. Isso significa que compartilhar estado entre frameworks será tranquilo! Astro é feito para ser flexível, então adoramos soluções que oferecem uma experiência de desenvolvedor semelhante independente de sua preferência.
Mesmo assim, há várias alternativas que você pode explorar. Elas são:
Perguntas frequentes
🙋 Posso utilizar Nano Stores em arquivos .astro
ou em outros componentes no lado do servidor? Nano Stores podem ser importadas, escritas para e lidas de componentes no lado do servidor, porém nós não recomendamos! Isso por conta de algumas restrições:
Escrever para uma store de um arquivo .astro
ou componente não-hidratado não irá afetar os valores recebidos por componentes no lado do cliente .
Você não pode passar uma Nano Store como uma “prop” para componentes no lado do cliente.
Você não pode inscrever-se para mudanças em uma store de um arquivo .astro
, já que componentes Astro não são re-renderizados.
Se você entende estas restrições e ainda tem um caso de uso, você pode tentar dar uma chance as Nano Stores! Apenas lembre-se de que Nano Stores são feitas para reatividade em mudanças especificadamente no cliente .
🙋 Como Svelte stores se comparam a Nano Stores? Nano Stores e Svelte stores são bastante similares! Na realidade, nanostores te permite utilizar o mesmo atalho $
para inscrições que você pode utilizar em Svelte stores.
Se você quiser evitar bibliotecas de terceiros, Svelte stores são uma ótima ferramenta de comunicação entre ilhas por si. Mesmo assim, você pode preferir Nano Stores se a) você gosta de seus add-ons para “objetos” e estado async , ou b) você quer se comunicar entre Svelte e outros frameworks de UI como Preact ou Vue.
🙋 Como signals do Solid se comparam a Nano Stores? Se você utilizou Solid por um tempo, você deve ter tentado mover signals ou stores para fora de seus componentes. Esta é uma ótima forma de compartilhar estado entre ilhas de Solid! Tente exportar signals de um arquivo compartilhado:
import { createSignal } from ' solid-js ' ;
export const contagemCompartilhada = createSignal ( 0 );
…e todos os componentes importante contagemCompartilhada
irão compartilhar o mesmo estado. Apesar disso funcionar bem, você pode preferir Nano Stores se a) você gosta de seus add-ons para “objetos” e estado async , ou b) você quer se comunicar entre Solid e outros frameworks de UI como Preact ou Vue.
Instalando Nano Stores
Para começar, instale Nano Stores junto de seu pacote auxiliar para seu framework de UI 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
Você pode dar uma olhada no guia de uso do Nano Stores a partir daqui ou seguir com nosso exemplo abaixo!
Exemplo de uso - flyout de carrinho de ecommerce
Vamos dizer que estamos construindo uma interface de ecommerce simples com três elementos interativos:
Um formulário de submissão de “adicionar ao carrinho”
Um flyout de carrinho para mostrar os itens adicionados
Um toggle do flyout de carrinho
Tente o exemplo completo em sua máquina ou online via Stackblitz.
Seu arquivo Astro base deve se parecer com isso:
---
import ToggleFlyoutCarrinho from ' ../components/ToggleFlyoutCarrinho ' ;
import FlyoutCarrinho from ' ../components/FlyoutCarrinho ' ;
import FormAdicionarAoCarrinho from ' ../components/FormAdicionarAoCarrinho ' ;
---
<! DOCTYPE html >
< html lang = " pt-BR " >
< head > ... </ head >
< body >
< header >
< nav >
< a href = " / " > Loja Astro </ a >
< ToggleFlyoutCarrinho client:load />
</ nav >
</ header >
< main >
< FormAdicionarAoCarrinho client:load >
<!-- ... -->
</ FormAdicionarAoCarrinho >
</ main >
< FlyoutCarrinho client:load />
</ body >
</ html >
Utilizando “atoms”
Vamos começar por abrir FlyoutCarrinho
sempre que ToggleFlyoutCarrinho
é clicado.
Primeiro, crie um novo arquivo JS ou TS para conter sua store. Nós iremos utilizar um “atom” para isso:
import { atom } from ' nanostores ' ;
export const isCarrinhoAberto = atom ( false );
Agora, nós podemos importar esta store em qualquer arquivo que precisa ser lido ou escrito. Iremos começar a conectando em nosso ToggleFlyoutCarrinho
:
import { useStore } from ' @nanostores/preact ' ;
import { isCarrinhoAberto } from ' ../storeCarrinho ' ;
export default function BotaoCarrinho () {
// leia o valor da store com o hook `useStore`
const $isCarrinhoAberto = useStore ( isCarrinhoAberto );
// escreva para a store importada utilizando `.set`
return (
< button onClick = { () => isCarrinhoAberto . set ( ! $isCarrinhoAberto ) } > Carrinho </ button >
)
}
import { useStore } from ' @nanostores/react ' ;
import { isCarrinhoAberto } from ' ../storeCarrinho ' ;
export default function BotaoCarrinho () {
// leia o valor da store com o hook `useStore`
const $isCarrinhoAberto = useStore ( isCarrinhoAberto );
// escreva para a store importada utilizando `.set`
return (
< button onClick = { () => isCarrinhoAberto . set ( ! $isCarrinhoAberto ) } > Carrinho </ button >
)
}
import { useStore } from ' @nanostores/solid ' ;
import { isCarrinhoAberto } from ' ../storeCarrinho ' ;
export default function BotaoCarrinho () {
// leia o valor da store com o hook `useStore`
const $isCarrinhoAberto = useStore ( isCarrinhoAberto );
// escreva para a store importada utilizando `.set`
return (
< button onClick = { () => isCarrinhoAberto . set ( ! $isCarrinhoAberto ()) } > Carrinho </ button >
)
}
< script >
import { isCarrinhoAberto } from ' ../storeCarrinho ' ;
</ script >
<!--utilize "$" para ler o valor da store -->
< button on :click= { () => isCarrinhoAberto . set ( ! $ isCarrinhoAberto) } > Carrinho </ button >
< template >
<!--escreva para a store importada utilizando `.set`-->
< button @click = " isCarrinhoAberto.set(!$isCarrinhoAberto) " > Carrinho </ button >
</ template >
< script setup >
import { isCarrinhoAberto } from ' ../storeCarrinho ' ;
import { useStore } from ' @nanostores/vue ' ;
// leia o valor da store com o hook `useStore`
const $isCarrinhoAberto = useStore ( isCarrinhoAberto );
</ script >
import { LitElement, html } from ' lit ' ;
import { isCarrinhoAberto } from ' ../storeCarrinho ' ;
export class ToggleFlyoutCarrinho extends LitElement {
tratarClique () {
isCarrinhoAberto . set ( ! isCarrinhoAberto . get ());
}
render () {
return html `
<button @click=" ${ this . tratarClique } ">Cart</button>
` ;
}
}
customElements . define ( ' toggle-flyout-carrinho ' , ToggleFlyoutCarrinho);
Após, podemos ler isCarrinhoAberto
de nosso componente FlyoutCarrinho
:
import { useStore } from ' @nanostores/preact ' ;
import { isCarrinhoAberto } from ' ../storeCarrinho ' ;
export default function FlyoutCarrinho () {
const $isCarrinhoAberto = useStore ( isCarrinhoAberto );
return $isCarrinhoAberto ? < aside > ... </ aside > : null ;
}
import { useStore } from ' @nanostores/react ' ;
import { isCarrinhoAberto } from ' ../storeCarrinho ' ;
export default function FlyoutCarrinho () {
const $isCarrinhoAberto = useStore ( isCarrinhoAberto );
return $isCarrinhoAberto ? < aside > ... </ aside > : null ;
}
import { useStore } from ' @nanostores/solid ' ;
import { isCarrinhoAberto } from ' ../storeCarrinho ' ;
export default function FlyoutCarrinho () {
const $isCarrinhoAberto = useStore ( isCarrinhoAberto );
return $isCarrinhoAberto () ? < aside > ... </ aside > : null ;
}
< script >
import { isCarrinhoAberto } from ' ../storeCarrinho ' ;
</ script >
{# if $isCarrinhoAberto}
< aside > ... </ aside >
{/ if }
< template >
< aside v-if = " $isCarrinhoAberto " > ... </ aside >
</ template >
< script setup >
import { isCarrinhoAberto } from ' ../storeCarrinho ' ;
import { useStore } from ' @nanostores/vue ' ;
const $isCarrinhoAberto = useStore ( isCarrinhoAberto );
</ script >
import { isCarrinhoAberto } from ' ../storeCarrinho ' ;
import { LitElement, html } from ' lit ' ;
import { StoreController } from ' @nanostores/lit ' ;
export class FlyoutCarrinho extends LitElement {
private carrinhoAberto = new StoreController ( this , isCarrinhoAberto);
render () {
return this . carrinhoAberto . value ? html ` <aside>...</aside> ` : null ;
}
}
customElements . define ( ' flyout-carrinho ' , FlyoutCarrinho);
Utilizando “maps”
Agora, vamos rastrear os itens dentro de seu carrinho. Para evitar duplicações e continuar rastreando a “quantidade,” nós podemos armazenar seu carrinho como um objeto com o ID do item como uma chave. Nós iremos utilizar um Map para isso.
Vamos adicionar uma store itensCarrinho
a nossa storeCarrinho.js
de anteriormente. Você também pode trocar para um arquivo TypeScript para definir a forma se quiser.
import { atom, map } from ' nanostores ' ;
export const isCarrinhoAberto = atom ( false );
/**
* @typedef {Object} ItemCarrinho
* @property {string} id
* @property {string} nome
* @property {string} srcImagem
* @property {number} quantidade
*/
/** @type {import('nanostores').MapStore<Record<string, ItemCarrinho>>} */
export const itensCarrinho = map ( {} );
import { atom, map } from ' nanostores ' ;
export const isCarrinhoAberto = atom ( false );
export type ItemCarrinho = {
id : string ;
nome : string ;
srcImagem : string ;
quantidade : number ;
}
export const itensCarrinho = map < Record < string , ItemCarrinho >> ( {} );
Agora, vamos exportar um helper adicionarItemCarrinho
para nossos componentes utilizarem.
Se o item não existe em seu carrinho , adicione o item com uma quantidade inicial de 1.
Se o item já existe , aumente a quantidade em 1.
...
export function adicionarItemCarrinho ( { id , nome , srcImagem } ) {
const entradaExistente = itensCarrinho . get ()[ id ];
if ( entradaExistente ) {
itensCarrinho . setKey ( id , {
... entradaExistente ,
quantidade: entradaExistente . quantidade + 1 ,
})
} else {
itensCarrinho . setKey (
id ,
{ id , nome , srcImagem , quantidade: 1 }
);
}
}
...
type InfoVisivelItem = Pick < ItemCarrinho , ' id ' | ' nome ' | ' srcImagem ' >;
export function adicionarItemCarrinho ( { id , nome , srcImagem } : InfoVisivelItem ) {
const entradaExistente = itensCarrinho . get ()[id];
if (entradaExistente) {
itensCarrinho . setKey (id , {
... entradaExistente ,
quantidade: entradaExistente . quantidade + 1 ,
});
} else {
itensCarrinho . setKey (
id ,
{ id , nome , srcImagem , quantidade: 1 }
);
}
}
Nota
🙋 Por que utilizar .get()
aqui ao invés do helper useStore
? Você deve ter notado que estamos chamando itensCarrinho.get()
aqui ao invés de pegar o helper useStore
de nossos exemplos em React / Preact / Solid / Vue. Isso é porque useStore é feito para acionar re-renderizações de componente. Em outras palavras, useStore
deve ser utilizado sempre que o valor da store está sendo renderizado na UI. Já que estamos lendo o valor quando um evento é iniciado (adicionarAoCarrinho
nesse caso) e nós não estamos tentando renderizar o valor, nós não precisamos de useStore
aqui.
Com nossa store no lugar, nós podemos chamar essa função dentro do nosso FormAdicionarAoCarrinho
sempre que o formulário é enviado. Nós também iremos abrir o flyout do carrinho para que você veja um resumo completo do carrinho.
import { adicionarItemCarrinho, isCarrinhoAberto } from ' ../storeCarrinho ' ;
export default function FormAdicionarAoCarrinho ( { children } ) {
// nós iremos fazer hardcode da informação do item para simplificar!
const infoItemHardcoded = {
id: ' estatueta-astronauta ' ,
nome: ' Estatueta de Astronauta ' ,
srcImagem: ' /imagens/estatueta-astronauta.png ' ,
}
function adicionarAoCarrinho ( e ) {
e . preventDefault ();
isCarrinhoAberto . set ( true );
adicionarItemCarrinho ( infoItemHardcoded );
}
return (
< form onSubmit = { adicionarAoCarrinho } >
{ children }
</ form >
)
}
import { adicionarItemCarrinho, isCarrinhoAberto } from ' ../storeCarrinho ' ;
export default function FormAdicionarAoCarrinho ( { children } ) {
// nós iremos fazer hardcode da informação do item para simplificar!
const infoItemHardcoded = {
id: ' estatueta-astronauta ' ,
nome: ' Estatueta de Astronauta ' ,
srcImagem: ' /imagens/estatueta-astronauta.png ' ,
}
function adicionarAoCarrinho ( e ) {
e . preventDefault ();
isCarrinhoAberto . set ( true );
adicionarItemCarrinho ( infoItemHardcoded );
}
return (
< form onSubmit = { adicionarAoCarrinho } >
{ children }
</ form >
)
}
import { adicionarItemCarrinho, isCarrinhoAberto } from ' ../storeCarrinho ' ;
export default function FormAdicionarAoCarrinho ( { children } ) {
// nós iremos fazer hardcode da informação do item para simplificar!
const infoItemHardcoded = {
id: ' estatueta-astronauta ' ,
nome: ' Estatueta de Astronauta ' ,
srcImagem: ' /imagens/estatueta-astronauta.png ' ,
}
function adicionarAoCarrinho ( e ) {
e . preventDefault ();
isCarrinhoAberto . set ( true );
adicionarItemCarrinho ( infoItemHardcoded );
}
return (
< form onSubmit = { adicionarAoCarrinho } >
{ children }
</ form >
)
}
< form on :submit| preventDefault = { adicionarAoCarrinho } >
< slot ></ slot >
</ form >
< script >
import { adicionarItemCarrinho, isCarrinhoAberto } from ' ../storeCarrinho ' ;
// nós iremos fazer hardcode da informação do item para simplificar!
const infoItemHardcoded = {
id: ' estatueta-astronauta ' ,
nome: ' Estatueta de Astronauta ' ,
srcImagem: ' /imagens/estatueta-astronauta.png ' ,
}
function adicionarAoCarrinho () {
isCarrinhoAberto . set ( true );
adicionarItemCarrinho ( infoItemHardcoded );
}
</ script >
< template >
< form @submit = " adicionarAoCarrinho " >
< slot ></ slot >
</ form >
</ template >
< script setup >
import { adicionarItemCarrinho, isCarrinhoAberto } from ' ../storeCarrinho ' ;
// nós iremos fazer hardcode da informação do item para simplificar!
const infoItemHardcoded = {
id: ' estatueta-astronauta ' ,
nome: ' Estatueta de Astronauta ' ,
srcImagem: ' /imagens/estatueta-astronauta.png ' ,
}
function adicionarAoCarrinho ( e ) {
e . preventDefault ();
isCarrinhoAberto . set ( true );
adicionarItemCarrinho ( infoItemHardcoded );
}
</ script >
import { LitElement, html } from ' lit ' ;
import { isCarrinhoAberto, adicionarItemCarrinho } from ' ../storeCarrinho ' ;
export class FormAdicionarAoCarrinho extends LitElement {
static get properties () {
return {
item: { type: Object },
};
}
constructor () {
super ();
this . item = {};
}
adicionarAoCarrinho ( e ) {
e . preventDefault ();
isCarrinhoAberto . set ( true );
adicionarItemCarrinho ( this . item );
}
render () {
return html `
<form @submit=" ${ this . adicionarAoCarrinho } ">
<slot></slot>
</form>
` ;
}
}
customElements . define ( ' form-adicionar-ao-carrinho ' , FormAdicionarAoCarrinho);
Finalmente, iremos renderizar esses itens do carrinho dentro do nosso FlyoutCarrinho
:
import { useStore } from ' @nanostores/preact ' ;
import { isCarrinhoAberto, itensCarrinho } from ' ../storeCarrinho ' ;
export default function FlyoutCarrinho () {
const $isCarrinhoAberto = useStore ( isCarrinhoAberto );
const $itensCarrinho = useStore ( itensCarrinho );
return $isCarrinhoAberto ? (
< aside >
{ Object . values ( $itensCarrinho ) . length ? (
< ul >
{ Object . values ( $itensCarrinho ) . map ( itemCarrinho => (
< li >
< img src = { itemCarrinho . srcImagem } alt = { itemCarrinho . nome } />
< h3 > { itemCarrinho . nome } </ h3 >
< p > Quantidade: { itemCarrinho . quantidade } </ p >
</ li >
)) }
</ ul >
) : < p > Seu carrinho está vazio! </ p > }
</ aside >
) : null ;
}
import { useStore } from ' @nanostores/react ' ;
import { isCarrinhoAberto, itensCarrinho } from ' ../storeCarrinho ' ;
export default function FlyoutCarrinho () {
const $isCarrinhoAberto = useStore ( isCarrinhoAberto );
const $itensCarrinho = useStore ( itensCarrinho );
return $isCarrinhoAberto ? (
< aside >
{ Object . values ( $itensCarrinho ) . length ? (
< ul >
{ Object . values ( $itensCarrinho ) . map ( itemCarrinho => (
< li >
< img src = { itemCarrinho . srcImagem } alt = { itemCarrinho . nome } />
< h3 > { itemCarrinho . nome } </ h3 >
< p > Quantidade: { itemCarrinho . quantidade } </ p >
</ li >
)) }
</ ul >
) : < p > Seu carrinho está vazio! </ p > }
</ aside >
) : null ;
}
import { useStore } from ' @nanostores/solid ' ;
import { isCarrinhoAberto, itensCarrinho } from ' ../storeCarrinho ' ;
export default function FlyoutCarrinho () {
const $isCarrinhoAberto = useStore ( isCarrinhoAberto );
const $itensCarrinho = useStore ( itensCarrinho );
return $isCarrinhoAberto () ? (
< aside >
{ Object . values ( $itensCarrinho ()) . length ? (
< ul >
{ Object . values ( $itensCarrinho ()) . map ( itemCarrinho => (
< li >
< img src = { itemCarrinho . srcImagem } alt = { itemCarrinho . nome } />
< h3 > { itemCarrinho . nome } </ h3 >
< p > Quantidade: { itemCarrinho . quantidade } </ p >
</ li >
)) }
</ ul >
) : < p > Seu carrinho está vazio! </ p > }
</ aside >
) : null ;
}
< script >
import { isCarrinhoAberto, itensCarrinho } from ' ../storeCarrinho ' ;
</ script >
{# if $isCarrinhoAberto}
{# if Object . values ($itensCarrinho) . length }
< aside >
{# each Object . values ($itensCarrinho) as itemCarrinho}
< li >
< img src = { itemCarrinho . srcImagem } alt = { itemCarrinho . nome } />
< h3 > { itemCarrinho . nome } </ h3 >
< p > Quantidade: { itemCarrinho . quantidade } </ p >
</ li >
{/ each }
</ aside >
{# else }
< p > Seu carrinho está vazio! </ p >
{/ if }
{/ if }
< template >
< aside v-if = " $isCarrinhoAberto " >
< ul v-if = " Object.values($itensCarrinho).length " >
< li v-for = " itemCarrinho in Object.values($itensCarrinho) " v-bind:key = " itemCarrinho.nome " >
< img :src = itemCarrinho.srcImagem :alt = itemCarrinho.nome />
< h3 > {{itemCarrinho.nome}} </ h3 >
< p > Quantidade: {{itemCarrinho.quantidade}} </ p >
</ li >
</ ul >
< p v-else > Seu carrinho está vazio! </ p >
</ aside >
</ template >
< script setup >
import { itensCarrinho, isCarrinhoAberto } from ' ../storeCarrinho ' ;
import { useStore } from ' @nanostores/vue ' ;
const $isCarrinhoAberto = useStore ( isCarrinhoAberto );
const $itensCarrinho = useStore ( itensCarrinho );
</ script >
import { LitElement, html } from ' lit ' ;
import { isCarrinhoAberto, itensCarrinho } from ' ../storeCarrinho ' ;
import { StoreController } from ' @nanostores/lit ' ;
export class FlyoutCarrinhoLit extends LitElement {
private carrinhoAberto = new StoreController ( this , isCarrinhoAberto);
private obterItensCarrinho = new StoreController ( this , itensCarrinho);
renderizarItemCarrinho ( itemCarrinho ) {
return html `
<li>
<img src=" ${ itemCarrinho . srcImagem } " alt=" ${ itemCarrinho . nome } " />
<h3> ${ itemCarrinho . nome } </h3>
<p>Quantidade: ${ itemCarrinho . quantidade } </p>
</li>
` ;
}
render () {
return this . carrinhoAberto . value
? html `
<aside>
${
Object . values ( this . obterItensCarrinho . value ) . length
? html `
<ul>
${ Object . values ( this . obterItensCarrinho . value ) . map ( ( itemCarrinho ) =>
this . renderizarItemCarrinho (itemCarrinho)
) }
</ul>
`
: html ` <p>Seu carrinho está vazio!</p> `
}
</aside>
`
: null ;
}
}
customElements . define ( ' flyout-carrinho ' , FlyoutCarrinhoLit);
Agora, você deve ter um exemplo completo de ecommerce interativo com o menor bundle de JS na galáxia 🚀
Tente o exemplo completo em sua máquina ou online via Stackblitz!
Learn