Pular para o conteúdo

Compartilhe Estado Entre Ilhas

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:

Instalando Nano Stores

Para começar, instale Nano Stores junto de seu pacote auxiliar para seu framework de UI favorito:

Terminal window
npm install nanostores @nanostores/preact

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:

src/pages/index.astro
---
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:

src/storeCarrinho.js
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:

src/components/ToggleFlyoutCarrinho.jsx
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>
)
}

Após, podemos ler isCarrinhoAberto de nosso componente FlyoutCarrinho:

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

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.

src/storeCarrinho.js
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({});

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 existe, aumente a quantidade em 1.
src/storeCarrinho.js
...
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 }
);
}
}

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.

src/components/FormAdicionarAoCarrinho.jsx
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>
)
}

Finalmente, iremos renderizar esses itens do carrinho dentro do nosso FlyoutCarrinho:

src/components/FlyoutCarrinho.jsx
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;
}

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!