Gestione dello stato in Astro: Un approcio approfondito ai Nanostores

Benvenuti alla prima parte della serie “Nanostores in Astro: A Multi-Framework Adventure”. Se sei stato a fatica con la gestione dello stato nei tuoi progetti Astro con più framework, sei nel posto giusto. Oggi, esploriamo Nanostores, una soluzione leggera per la gestione dello stato che integra in modo fluido l’architettura di isolotti di componenti di Astro.

In questo articolo, scopriremo come Nanostores può semplificare la gestione dello stato attraverso Astro, React, Vue e Svelte. Coveriremo lo stato indipendente, lo stato condiviso e introdurremo la gestione dello stato persistente. Cominciamo questa giornata per semplificare la gestione dello stato nei tuoi progetti Astro.

Se sei pronto ad affrontare direttamente il materiale, ecco un breve riepilogo e alcuni link essenziali:

Siate liberi di esplorare la demo e il codice insieme a questo articolo per una esperienza di apprendimento pratico!

Nanostores è una libreria di gestione dello stato minimalista progettata con l’agnosticità del framework in mente. Fornisce un’API semplice per la creazione e la gestione di pezzi atomici dello stato, che possono essere facilmente condivisi e aggiornati in diversi punti dell’applicazione.

  1. Leggero e veloce: Nanostores è incredibilmente piccolo (tra i 265 e i 814 byte), garantendo che non incrementerà la dimensione del pacchetto.

  2. Agnostico al framework: perfetto per l’ecosistema multi-framework di Astro. Integra agevolmente React, Vue, Svelte, Solid e JavaScript vanilla.

  3. API semplice: Nessun setup complesso o boilerplate. È semplice e intuitivo da utilizzare.

  4. Complementare agli isolati componenti di Astro: Nanostores migliora l’architettura delle isole di Astro, permettendo una gestione efficiente dello stato attraverso componenti interattive isolate.

Nanostores ruota attorno a tre concetti principali:

  1. Atomi: Negozi semplici che contengono un solo valore.

  2. Mappe: Negozi che contengono oggetti con diverse proprietà.

  3. Negozi Calcolati: Negozi derivati che calcolano il loro valore in base ad altri negozi.

Adesso diamo un’occhiata ad un esempio veloce:

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!'
)

In questo snippet, abbiamo creato un atomo per un contatore, una mappa per i dati utente e un negozio calcolato per un saluto dinamico. Semplice, non è?

Iniziare con Nanostores nel tuo progetto Astro è facile. Ecco come lo fai:

  1. Prima di tutto, installa Nanostores e le sue integrazioni con il 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. Crea un nuovo file per i tuoi negozi, diciamo 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. Ora puoi usare questo negozio in qualsiasi componente tuo. Ecco un esempio rapido in un 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>

Ecco qui! Hai appena impostato un Nanostore nel tuo progetto Astro.

In progetti Astro multi-framework, potrebbe voler gestire lo stato in modo indipendente all’interno di componenti di framework differenti. Nanostores rende questo processo fluido. Esploriamo come implementare la gestione dello stato indipendente attraverso React, Vue, Svelte e componenti Astro.

Implementeremo un semplice contatore in ogni framework per dimostrare la gestione dello stato indipendente.

Prima di tutto, creiamo il nostro contatore indipendente:

// 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)
}

Ora, implementiamo questo contatore in ogni 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>

Come vedete, ogni componente del framework mantiene il suo proprio contatore dello stato indipendente utilizzando Nanostores. Questo approcio consente una gestione dello stato isolata all’interno di ogni componente, nonostante il framework utilizzato.

Adesso, esploriamo come Nanostores consente la condivisione dello stato tra componenti di framework differenti. Questo approcio è particolarmente utile quando è necessario sincronizzare lo stato tra diversi punti dell’applicazione.

Creiamo un contatore condiviso che possa essere aggiornato e mostrato in React, Vue, Svelte e componenti Astro.

Prima di tutto, creiamo il nostro contatore condiviso:

// 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)
}

Ora, implementiamo componenti in ogni framework che usano questo stato condiviso:

// 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>

Con questa configurazione, tutti questi componenti condivideranno lo stesso stato del contatore. Incrementando o decrementando il contatore in qualsiasi componente aggiorna il valore in tutti i componenti, indipendentemente dalla struttura di framework utilizzata.

Anche se gli stati indipendenti e condivisi sono potenti, a volte abbiamo bisogno che lo stato persista attraverso il ricaricamento della pagina o persino attraverso le sessioni del browser. Ecco come entra in gioco @nanostores/persistent. Scopriamo come implementare uno stato persistente nel nostro progetto Astro.

Prima di tutto, dobbiamo installare l’estensione dello stato persistente per Nanostores:

# Using npm
npm install @nanostores/persistent

# Using yarn
yarn add @nanostores/persistent

# Using pnpm
pnpm add @nanostores/persistent

Ora, creiamo un contatore persistente che mantiene il suo valore anche quando la pagina viene刷新:

// 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)
}

In questo esempio, ‘persistentCount’ è la chiave utilizzata per memorizzare il valore in localStorage, e 0 è il valore iniziale.

Implementiamo un contatore persistente usando componenti da framework differenti. Questo contatore mantiene il suo valore attraverso il ricaricamento della pagina e è accessibile da qualsiasi 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>

Ora, puoi usare questi componenti insieme in una pagina 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>

Questa configurazione dimostra un contatore persistente in cui:

  • React gestisce l’incremento

  • Vue gestisce il decremento

  • Svelte mostra il conteggio attuale

  • Astro fornisce un pulsante di reset

Il valore del contatore verrà mantenuto anche dopo il caricamento della pagina, dimostrando il potere di @nanostores/persistent nel mantenere lo stato.

Lo stato persistente è particolarmente utile per:

  1. Preferenze utente (ad esempio, impostazioni del tema, scelte della lingua)

  2. Moduli incompleti (per prevenire la perdita di dati durante un ricaricamento imprevisto della pagina)

  3. Token di autenticazione (per mantenere le sessioni utente)

  4. Cache locale di dati accessibili con frequenza

Utilizzando @nanostores/persistent, puoi migliorare l’esperienza utente mantenendo i dati dello stato importanti attraverso il caricamento delle pagine e le sessioni del browser.

Nel integrare Nanostores nei tuoi progetti Astro, teniamo a mente queste migliori pratiche e consigli per sfruttare al massimo questa soluzione di gestione dello stato leggera.

  • Usa atom per stati semplici e singoli.

  • Utilizza map per stati simili a oggetti con molte proprietà.

  • Utilizza computed per stati derivati che dipendono da altri magazzini.

  • Utilizza persistentAtom o persistentMap quando è necessario che lo stato persista tra ricariche di pagina.

Invece di creare magazzini grandi e monolitici, preferisci magazzini più piccoli e focalizzati. Questo approcio migliora la manutenibilità e la performance permettendo aggiornamenti più granulari.

// 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 hai uno stato che dipende da altri pezzi di stato, usa magazzini computati. Questo aiuta a mantenere lo stato DRY (Don’t Repeat Yourself) e garantisce che lo stato derivato sia sempre aggiornato.

import { atom, computed } from 'nanostores'

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

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

Nanostores ha un ottimo supporto per TypeScript. Lo usa per catturare errori presto e migliorare l’esperienza del sviluppatore.

import { atom } from 'nanostores'

interface User {
  id: number
  name: string
}

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

Anche se Nanostores è leggero, considera la performance in applicazioni di grandi dimensioni. Usa la funzione batched per gruppare molti aggiornamenti del magazzino insieme, riducendo il numero di re-render.

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)
})

Quando utilizzi Nanostores in un progetto Astro multi-framework, cerca di mantenere la logica dello stato centrale indipendente dal framework. Questo facilita la condivisione dello stato tra diversi componenti del 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>
}

Sebbene i negozi persistenti siano potenti, usali con attenzione. Non tutti gli stati devono persistere tra le sessioni. L’uso eccessivo dei negozi persistenti può portare a comportamenti imprevisti e potenziali problemi di prestazioni.

Per un debug più facile, puoi usare la funzione onMount per registrare i cambiamenti di stato:

import { atom, onMount } from 'nanostores'

const count = atom(0)

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

Quando usi Nanostores in componenti che possono essere smontati, assicurati di pulire le sottoscrizioni per prevenire perdite di memoria.

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

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

  // Rest of the component
}

Seguendo queste migliori pratiche e consigli, sarai in grado di gestire efficacemente lo stato nei tuoi progetti Astro utilizzando Nanostores, indipendentemente da quali framework stai integrando.

Come abbiamo esplorato in questo articolo, Nanostores fornisce una soluzione potente e leggera per la gestione dello stato nei progetti Astro, soprattutto quando si lavora con più framework. Ricapitoliamo i punti chiave:

  1. Versatilità: Nanostores si integra perfettamente con Astro, React, Vue, Svelte e Solid, rendendolo una scelta ideale per progetti multi-framework.

  2. Semplicità

    : Offrendo una semplice API, Nanostores presenta una curva di apprendimento bassa mentre offre ancora capacità robuste di gestione dello stato.

  3. Flessibilità: Da semplici store atomici a stati computati complessi e persino storage persistente, Nanostores si adatta a una vasta gamma di necessità di gestione dello stato.
  4. Prestazioni: La sua natura leggera garantisce che Nanostores non svilupperà il tuo applicativo, mantenendo i benefici di prestazioni di Astro.
  5. Migliori Pratiche: Seguendo le linee guida che abbiamo discusso, come mantenere i store piccoli e focalizzati, sfruttare TypeScript e usare i store computati per gli stati derivati, puoi creare sistemi di gestione dello stato efficienti e facili da mantenere.

Nanostores brilla all’interno dell’architettura delle isole componenti di Astro, permettendogli di gestire lo stato attraverso componenti interattive isolate in modo efficiente. Non importa se stai costruendo un semplice sito web con pochi elementi interattivi o un’applicazione web complessa con molti framework, Nanostores fornisce le tool necessarie per gestire lo stato in modo efficace.

Continuando la tua giornata con Astro e Nanostores, ricorda che il modo migliore per imparare è facendo. Sperimenta con diversi tipi di store, prova a implementare uno stato condiviso tra framework e esplora le possibilità di archiviazione persistente. Ogni progetto porterà nuove sfide e opportunità per raffinare le tue abilità nella gestione dello stato.

Stai attento agli articoli successivi della nostra serie “Nanostores in Astro: A Multi-Framework Adventure”, in cui ci avviamo più a fondo negli applicativi pratici e negli sviluppi tecnici avanzati per la gestione dello stato negli sviluppi di Astro.

Per approfondire la tua comprensione di Nanostores e del suo utilizzo in progetti di Astro, guarda queste risorse preziose:

Buon codice, e che i tuoi progetti Astro siano sempre statici e performanti!

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