Gerenciamento de estado em Astro: uma profunda análise dos nanostores

Seja bem-vindo ao primeiro episódio da série “Nanostores in Astro: A Multi-Framework Adventure”. Se você tiver dificuldades com o gerenciamento de estado em seus projetos Astro multi-framework, você vai adorar. Hoje, nós estaremos explorando Nanostores, uma solução de gerenciamento de estado leve que se integra perfeitamente com a arquitetura de ilhas de componentes do Astro.

Neste artigo, nós vamos mergulhar em como Nanostores pode simplificar o gerenciamento de estado entre componentes Astro, React, Vue e Svelte. Vamos cobrir estado independente, estado compartilhado e apresentar gerenciamento de estado persistente. Vamos embarcar nesta viagem para simplificar o gerenciamento de estado em nossos projetos Astro.

Se você está ansioso para mergulhar diretamente, aqui está um resumo rápido e alguns links essenciais:

Feel free to explore the demo and code alongside this article for a hands-on learning experience!

Nanostores é uma biblioteca de gerenciamento de estado minimalista projetada com agnosticismo de framework em mente. Ela fornece uma API simples para criar e gerenciar pequenos pedaços de estado atômicos, que podem ser facilmente compartilhados e atualizados em diferentes partes da sua aplicação.

  1. Leve e Rápido: Nanostores é incrivelmente pequeno (Entre 265 e 814 bytes), garantindo que não aumentará o tamanho do pacote de sua aplicação.

  2. Agnóstico de Framework: Perfeito para o ecossistema multi-framework de Astro. Integra-se com facilidade com React, Vue, Svelte, Solid e JavaScript vanilla.

  3. API Simples: Sem configuração complexa ou boilerplate. É simples e intuitivo de usar.

  4. Complementa as Ilhas de Componentes do Astro: Nanostores melhora a arquitetura de ilhas de componentes do Astro, permitindo o gerenciamento eficiente de estado em componentes interativos isolados.

Nanostores se baseia em três conceitos principais:

  1. Átomos: Armazenamentos simples que mantêm um valor único.

  2. Mapas: Armazenamentos que mantêm objetos com várias propriedades.

  3. Armazenamentos Computados: Armazenamentos derivados que calculam seu valor com base em outros armazenamentos.

Vamos ver um exemplo rápido:

import { atom, map, computed } from 'nanostores'

// Atom
const count = atom(0)

// Map
const user = map({ name: 'Astro Fan', isLoggedIn: false })

// Computed Store
const greeting = computed([user], (user) => 
  user.isLoggedIn ? `Welcome back, ${user.name}!` : 'Hello, guest!'
)

Neste snippet, criamos um átomo para um contador, um mapa para dados do usuário e um armazenamento computado para um cumprimento dinâmico. Simples, não é?

Iniciar com Nanostores no seu projeto Astro é fácil. Veja como fazer:

  1. Primeiro, instale Nanostores e suas integrações de framework:
# Using npm
npm install nanostores
npm install @nanostores/react  # For React
npm install @nanostores/vue    # For Vue

# Using yarn
yarn add nanostores
yarn add @nanostores/react     # For React
yarn add @nanostores/vue       # For Vue

# Using pnpm
pnpm add nanostores
pnpm add @nanostores/react     # For React
pnpm add @nanostores/vue       # For Vue

# Note: Svelte doesn't require a separate integration
  1. Crie um novo arquivo para seus armazenamentos, digamos src/stores/counterStore.js:
import { atom } from 'nanostores'

export const count = atom(0)

export function increment() {
  count.set(count.get() + 1)
}

export function decrement() {
  count.set(count.get() - 1)
}
  1. Agora você pode usar este armazenamento em qualquer componente dele. Aqui está um exemplo rápido em um componente Astro:
---
import { count, increment, decrement } from '../stores/counterStore'
---

<div>
  <button onclick={decrement}>-</button>
  <span>{count.get()}</span>
  <button onclick={increment}>+</button>
</div>

<script>
  import { count } from '../stores/counterStore'

  count.subscribe(value => {
    document.querySelector('span').textContent = value
  })
</script>

E lá você tem! Você acabou de configurar um Nanostore em seu projeto Astro.

Em projetos Astro multi-framework, você pode querer gerenciar estados independentemente dentro de componentes de frameworks diferentes. O Nanostores torna isso sinuoso. Vamos explorar como implementar o gerenciamento de estado independente através de componentes React, Vue, Svelte e Astro.

Nós implementaremos um simples contador em cada framework para demonstrar o gerenciamento de estado independente.

Primeiro, vamos criar nosso contador independente:

// src/stores/independentCounterStore.js
import { atom } from 'nanostores'

export const reactCount = atom(0)
export const vueCount = atom(0)
export const svelteCount = atom(0)
export const astroCount = atom(0)

export function increment(store) {
  store.set(store.get() + 1)
}

export function decrement(store) {
  store.set(store.get() - 1)
}

Agora, vamos implementar este contador em cada framework:

// src/components/ReactCounter.jsx
import { useStore } from '@nanostores/react'
import { reactCount, increment, decrement } from '../stores/independentCounterStore'

export function ReactCounter() {
  const count = useStore(reactCount)

  return (
    <div>
      <button onClick={() => decrement(reactCount)}>-</button>
      <span>{count}</span>
      <button onClick={() => increment(reactCount)}>+</button>
    </div>
  )
}
<!-- src/components/VueCounter.vue -->
<template>
  <div>
    <button @click="decrement(vueCount)">-</button>
    <span>{{ count }}</span>
    <button @click="increment(vueCount)">+</button>
  </div>
</template>

<script setup>
import { useStore } from '@nanostores/vue'
import { vueCount, increment, decrement } from '../stores/independentCounterStore'

const count = useStore(vueCount)
</script>
<!-- src/components/SvelteCounter.svelte -->
<script>
import { svelteCount, increment, decrement } from '../stores/independentCounterStore'
</script>

<div>
  <button on:click={() => decrement(svelteCount)}>-</button>
  <span>{$svelteCount}</span>
  <button on:click={() => increment(svelteCount)}>+</button>
</div>
---
import { astroCount, increment, decrement } from '../stores/independentCounterStore'
---

<div>
  <button id="decrement">-</button>
  <span id="count">{astroCount.get()}</span>
  <button id="increment">+</button>
</div>

<script>
  import { astroCount, increment, decrement } from '../stores/independentCounterStore'

  document.getElementById('decrement').addEventListener('click', () => decrement(astroCount))
  document.getElementById('increment').addEventListener('click', () => increment(astroCount))

  astroCount.subscribe(value => {
    document.getElementById('count').textContent = value
  })
</script>

Como você pode ver, cada componente de framework mantém seu próprio contador estado independente usando Nanostores. Esta abordagem permite o gerenciamento de estado isolado dentro de cada componente, independentemente do framework usado.

Agora, vamos explorar como o Nanostores permite o estado compartilhado entre componentes de frameworks diferentes. Isso é particularmente útil quando você precisa sincronizar estados entre várias partes de sua aplicação.

Nós criaremos um contador compartilhado que possa ser atualizado e exibido através de componentes React, Vue, Svelte e Astro.

Primeiro, vamos criar nosso contador compartilhado:

// src/stores/sharedCounterStore.js
import { atom } from 'nanostores'

export const sharedCount = atom(0)

export function increment() {
  sharedCount.set(sharedCount.get() + 1)
}

export function decrement() {
  sharedCount.set(sharedCount.get() - 1)
}

Agora, vamos implementar componentes em cada framework que usam este estado compartilhado:

// src/components/ReactSharedCounter.jsx
import { useStore } from '@nanostores/react'
import { sharedCount, increment, decrement } from '../stores/sharedCounterStore'

export function ReactSharedCounter() {
  const count = useStore(sharedCount)

  return (
    <div>
      <h2>React Shared Counter</h2>
      <button onClick={decrement}>-</button>
      <span>{count}</span>
      <button onClick={increment}>+</button>
    </div>
  )
}
<!-- src/components/VueSharedCounter.vue -->
<template>
  <div>
    <h2>Vue Shared Counter</h2>
    <button @click="decrement">-</button>
    <span>{{ count }}</span>
    <button @click="increment">+</button>
  </div>
</template>

<script setup>
import { useStore } from '@nanostores/vue'
import { sharedCount, increment, decrement } from '../stores/sharedCounterStore'

const count = useStore(sharedCount)
</script>
<!-- src/components/SvelteSharedCounter.svelte -->
<script>
import { sharedCount, increment, decrement } from '../stores/sharedCounterStore'
</script>

<div>
  <h2>Svelte Shared Counter</h2>
  <button on:click={decrement}>-</button>
  <span>{$sharedCount}</span>
  <button on:click={increment}>+</button>
</div>
---
import { sharedCount, increment, decrement } from '../stores/sharedCounterStore'
---

<div>
  <h2>Astro Shared Counter</h2>
  <button id="shared-decrement">-</button>
  <span id="shared-count">{sharedCount.get()}</span>
  <button id="shared-increment">+</button>
</div>

<script>
  import { sharedCount, increment, decrement } from '../stores/sharedCounterStore'

  document.getElementById('shared-decrement').addEventListener('click', decrement)
  document.getElementById('shared-increment').addEventListener('click', increment)

  sharedCount.subscribe(value => {
    document.getElementById('shared-count').textContent = value
  })
</script>

Com esta configuração, todos esses componentes irão compartilhar o mesmo estado do contador. Incrementar ou decrementar o contador em qualquer componente atualizará o valor em todos os componentes, independentemente do framework usado.

Enquanto estados independentes e compartilhados são poderosos, às vezes precisamos que o nosso estado persista através de recargas de página ou até mesmo por sessões do navegador. É aqui que o @nanostores/persistent entra em cena. Vamos explorar como implementar um estado persistente em nosso projeto Astro.

Primeiro, precisamos instalar o complemento persistente para Nanostores:

# Using npm
npm install @nanostores/persistent

# Using yarn
yarn add @nanostores/persistent

# Using pnpm
pnpm add @nanostores/persistent

Agora, vamos criar um contador persistente que manterá seu valor mesmo quando a página for recarregada:

// src/stores/persistentCounterStore.js
import { persistentAtom } from '@nanostores/persistent'

export const persistentCount = persistentAtom('persistentCount', 0)

export function increment() {
  persistentCount.set(persistentCount.get() + 1)
}

export function decrement() {
  persistentCount.set(persistentCount.get() - 1)
}

export function reset() {
  persistentCount.set(0)
}

Neste exemplo, ‘persistentCount’ é a chave usada para armazenar o valor no localStorage, e 0 é o valor inicial.

Vamos implementar um contador persistente usando componentes de frameworks diferentes. Este contador manterá seu valor através de recargas de página e será acessível de qualquer framework.

// src/components/ReactPersistentIncrement.jsx
import { useStore } from '@nanostores/react'
import { persistentCount, increment } from '../stores/persistentCounterStore'

export function ReactPersistentIncrement() {
  const count = useStore(persistentCount)

  return (
    <button onClick={increment}>
      React Increment: {count}
    </button>
  )
}
<!-- src/components/VuePersistentDecrement.vue -->
<template>
  <button @click="decrement">
    Vue Decrement: {{ count }}
  </button>
</template>

<script setup>
import { useStore } from '@nanostores/vue'
import { persistentCount, decrement } from '../stores/persistentCounterStore'

const count = useStore(persistentCount)
</script>
<!-- src/components/SveltePersistentDisplay.svelte -->
<script>
import { persistentCount } from '../stores/persistentCounterStore'
</script>

<div>
  Svelte Display: {$persistentCount}
</div>
---
import { reset } from '../stores/persistentCounterStore'
---

<button id="reset-button">Astro Reset</button>

<script>
  import { persistentCount, reset } from '../stores/persistentCounterStore'

  document.getElementById('reset-button').addEventListener('click', reset)

  persistentCount.subscribe(value => {
    console.log('Persistent count updated:', value)
  })
</script>

Agora, você pode usar esses componentes juntos em uma página Astro:

---
import ReactPersistentIncrement from '../components/ReactPersistentIncrement'
import VuePersistentDecrement from '../components/VuePersistentDecrement.vue'
import SveltePersistentDisplay from '../components/SveltePersistentDisplay.svelte'
---

<div>
  <h2>Persistent Counter Across Frameworks</h2>
  <ReactPersistentIncrement client:load />
  <VuePersistentDecrement client:load />
  <SveltePersistentDisplay client:load />
  <button id="reset-button">Astro Reset</button>
</div>

<script>
  import { persistentCount, reset } from '../stores/persistentCounterStore'

  document.getElementById('reset-button').addEventListener('click', reset)

  persistentCount.subscribe(value => {
    console.log('Persistent count updated:', value)
  })
</script>

Esta configuração demonstra um contador persistente onde:

  • React manipula o incremento

  • Vue manipula o decremento

  • Svelte exibe o contador atual

  • Astro fornece um botão de reset

O valor do contador persistirá entre recarregamentos de página, mostrando o poder do @nanostores/persistent na manutenção do estado.

O estado persistente é particularmente útil para:

  1. Preferências de usuário (ex.: configurações de tema, escolhas de idioma)

  2. Formulários parcialmente preenchidos (para evitar perda de dados em recarregamentos de página acidentais)

  3. Tokens de autenticação (para manter as sessões de usuário)

  4. Cache local de dados frequentemente acessados

Ao aproveitar o @nanostores/persistent, você pode melhorar a experiência do usuário mantendo dados de estado importante entre carregamentos de página e sessões do navegador.

Enquanto você integra Nanostores em seus projetos Astro, tenha em mente estas melhores práticas e dicas para aproveitar ao máximo esta solução de gerenciamento de estado leve.

  • Use atom para estados simples, de valor único.

  • Use map para estados de objeto com várias propriedades.

  • Use computed para estados derivados que dependem de outros armazéns.

  • Use persistentAtom ou persistentMap quando você precisa que o estado persista através de recarregamentos de página.

Em vez de criar armazéns grandes e monolíticos, prefira armazéns pequenos e focados. Esta abordagem melhora a manutenção e a performance permitindo atualizações mais granulares.

// Prefer this:
const userProfile = map({ name: '', email: '' })
const userPreferences = map({ theme: 'light', language: 'en' })

// Over this:
const user = map({ name: '', email: '', theme: 'light', language: 'en' })

Quando você tiver estado que depende de outros pedaços de estado, use armazéns computados. Isto ajuda a manter seu estado DRY (Não Repita Si Mesmo) e garante que o estado derivado está sempre atualizado.

import { atom, computed } from 'nanostores'

const firstName = atom('John')
const lastName = atom('Doe')

const fullName = computed(
  [firstName, lastName],
  (first, last) => `${first} ${last}`
)

Nanostores oferece excelente suporte a TypeScript. Use-o para capturar erros ao longo e melhorar a experiência do desenvolvedor.

import { atom } from 'nanostores'

interface User {
  id: number
  name: string
}

const currentUser = atom<User | null>(null)

Enquanto Nanostores é leve, tenha em mente o desempenho em aplicações maiores. Use a função batched para agrupar várias atualizações de armazém juntas, reduzindo o número de re-renderizações.

import { atom, batched } from 'nanostores'

const count1 = atom(0)
const count2 = atom(0)

export const incrementBoth = batched(() => {
  count1.set(count1.get() + 1)
  count2.set(count2.get() + 1)
})

Ao usar Nanostores em um projeto Astro com múltiplos frameworks, tente manter a lógica principal de estado independente do framework. Isso facilita o compartilhamento de estado entre diferentes componentes de framework.

// stores/themeStore.js
import { atom } from 'nanostores'

export const theme = atom('light')

export function toggleTheme() {
  theme.set(theme.get() === 'light' ? 'dark' : 'light')
}

// React component
import { useStore } from '@nanostores/react'
import { theme, toggleTheme } from '../stores/themeStore'

function ThemeToggle() {
  const currentTheme = useStore(theme)
  return <button onClick={toggleTheme}>{currentTheme}</button>
}

Embora os armazenamentos persistentes sejam poderosos, use-os de forma criteriosa. Nem todo estado precisa persistir entre sessões. O uso excessivo de armazenamentos persistentes pode levar a comportamentos inesperados e a possíveis problemas de desempenho.

Para facilitar a depuração, você pode usar a função onMount para registrar mudanças de estado:

import { atom, onMount } from 'nanostores'

const count = atom(0)

if (import.meta.env.DEV) {
  onMount(count, () => {
    count.listen((value) => {
      console.log('Count changed:', value)
    })
  })
}

Ao usar Nanostores em componentes que podem ser desmontados, certifique-se de limpar as assinaturas para evitar vazamentos de memória.

import { useEffect } from 'react'
import { count } from '../stores/countStore'

function Counter() {
  useEffect(() => {
    const unsubscribe = count.subscribe(() => {
      // Do something
    })
    return unsubscribe
  }, [])

  // Rest of the component
}

Seguindo essas melhores práticas e dicas, você será capaz de gerenciar efetivamente o estado em seus projetos Astro usando Nanostores, independentemente de quais frameworks você está integrando.

Como exploramos ao longo deste artigo, o Nanostores oferece uma solução poderosa e leve para gerenciamento de estado em projetos Astro, especialmente ao trabalhar com múltiplos frameworks. Vamos recapitular os principais pontos:

  1. Versatilidade: Nanostores se integra perfeitamente com Astro, React, Vue, Svelte e Solid, tornando-o uma escolha ideal para projetos com múltiplos frameworks.

  2. Simplicidade

    : Com sua API simples, as Nanostores oferecem uma curva de aprendizagem baixa enquanto ainda oferecem capacidades robustas de gerenciamento de estado.

  3. Flexibilidade: De lojas atômicas simples a estados computados complexos e até mesmo armazenamento persistente, as Nanostores se adapam a uma ampla variedade de necessidades de gerenciamento de estado.
  4. Performance: sua natureza leve garante que as Nanostores não vão encher seu aplicativo, mantendo os benefícios de performance do Astro.
  5. Melhores Práticas: Seguindo as diretrizes que discutimos, como manter as lojas pequenas e focadas, aproveitando o TypeScript e usando lojas computadas para estados derivados, você pode criar sistemas de gerenciamento de estado eficientes e manutenveis.

Nanostores brilha na arquitetura de ilhas de componentes do Astro, permitindo que você gerencie estado através de componentes interativos isolados eficientemente. Se você estiver construindo um site simples com alguns elementos interativos ou uma aplicação web complexa com vários frameworks, Nanostores fornece as ferramentas necessárias para gerenciar estado efetivamente.

Enquanto você continua sua jornada com Astro e Nanostores, lembre-se que a melhor forma de aprender é fazendo. Experimente diferentes tipos de loja, tente implementar estado compartilhado entre frameworks e explore as possibilidades de armazenamento persistente. Cada projeto trará novos desafios e oportunidades para refinar suas habilidades de gerenciamento de estado.

Fique de olho para os próximos artigos na nossa série “Nanostores no Astro: Uma Aventura com Multiframework”, onde nós mergulharmos mais fundo em aplicações práticas e técnicas avançadas para gerenciamento de estado em projetos Astro.

Para aprofundar seu entendimento de Nanostores e de sua utilização em projetos Astro, confira estas fontes valiosas:

Feliz codificação, e que seus projetos Astro sejam sempre estado eficientes!

Source:
https://meirjc.hashnode.dev/state-management-in-astro-a-deep-dive-into-nanostores