Gestión de estado en Astro: una profunda exploración de Nanostores

Bienvenido a la primera parte de la serie “Nanostores in Astro: A Multi-Framework Adventure”. Si has estado lidiando con el manejo de estado en tus proyectos Astro de multi-framework, tendrás un regalo. Hoy, exploraremos Nanostores, una solución de manejo de estado ligera que se integra sin problemas con la arquitectura de islas de componentes de Astro.

En este artículo, adentraremos cómo Nanostores puede simplificar el manejo de estado a través de componentes de Astro, React, Vue y Svelte. Cubriremos el estado independiente, el estado compartido y presentaremos el manejo de estado persistente. ¡Empecemos esta aventura para simplificar el manejo de estado en nuestros proyectos Astro!

Si estás ansioso por sumergirte directamente, aquí tienes un resumen rápido y algunos enlaces esenciales:

¡No dude en explorar la demostración y el código junto con este artículo para una experiencia de aprendizaje práctico!

Nanostores es una biblioteca de gestión de estado minimalista diseñada pensando en la agnosticidad de frameworks. Proporciona una API directa para crear y administrar pequeños trozos de estado atómicos, que pueden ser fácilmente compartidos y actualizados en diferentes partes de tu aplicación.

  1. Lightweight y rápido: Nanostores es increíblemente pequeño (Entre 265 y 814 bytes), garantizando que no empeorará el tamaño de tu paquete.

  2. Agnóstico respecto a los frameworks: Perfecto para el ecosistema multi-framework de Astro. Se integra sin problemas con React, Vue, Svelte, Solid y JavaScript puro.

  3. API Simple: No hay configuración compleja o boilerplate. Es directo e intuitivo de usar.

  4. Complementario a las Islas de Componentes de Astro: Nanostores refuerza la arquitectura de islas de Astro, permitiendo una eficiente gestión de estado a través de componentes interactivos aislados.

Nanostores se basa en tres conceptos principales:

  1. Atomos: Almacenes simples que contienen un solo valor.

  2. Mapas: Almacenes que contienen objetos con múltiples propiedades.

  3. Almacenes Computados: Almacenes derivados que calculan su valor basado en otros almacenes.

Veamos un ejemplo 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!'
)

En este fragmento, hemos creado un atomo para un contador, un mapa para los datos del usuario y un almacén computado para un saludo dinámico. ¿Sencillo, verdad?

Comenzar con Nanostores en tu proyecto Astro es directo. Aquí cómo hacerlo:

  1. Primero, instala Nanostores y sus integraciones con el 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 nuevo archivo para tus almacenes, 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. Ahora puedes usar este almacén en cualquiera de tus componentes. Aquí tienes un ejemplo rápido en un componente de 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>

Y allí tienes! Acabas de configurar un Nanostore en tu proyecto Astro.

En proyectos Astro con multi-framework, es posible que desee gestionar el estado de forma independiente dentro de componentes de diferentes frameworks. Nanostores hace que esta tarea sea transparente. Vamos a explorar cómo implementar la gestión de estado independiente entre componentes de React, Vue, Svelte y Astro.

Implementaremos un contador simple en cada framework para demostrar la gestión de estado independiente.

Primero, vamos a crear nuestro contador independiente de estado:

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

Ahora, implementaremos este contador en 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 puede ver, cada componente del framework mantiene su propio contador de estado independiente utilizando Nanostores. Este enfoque permite la gestión de estado aislado dentro de cada componente, sin importar el framework utilizado.

Ahora, exploraremos cómo Nanostores permite la gestión de estado compartido entre componentes de diferentes frameworks. Esto es particularmente útil cuando se necesita sincronizar estado entre diferentes partes de su aplicación.

Crearemos un contador compartido que pueda actualizarse y mostrarse entre componentes de React, Vue, Svelte y Astro.

Primero, vamos a crear nuestro almacén de contador compartido:

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

Ahora, implementaremos componentes en cada framework que utilicen este estado compartido:

// 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 este configurado, todos estos componentes compartirán el mismo estado del contador. Incrementar o decrementar el contador en cualquier componente actualizará el valor en todos los componentes, sin importar el marco utilizado.

Mientras que estados independientes y compartidos son poderosos, a veces necesitamos que nuestro estado persista a través de recargas de página o incluso sesiones de navegador. Aquí es donde entra en juego @nanostores/persistent. Exploremos cómo implementar un estado persistente en nuestro proyecto Astro.

Primero, necesitamos instalar el complemento de estado persistente para Nanostores:

# Using npm
npm install @nanostores/persistent

# Using yarn
yarn add @nanostores/persistent

# Using pnpm
pnpm add @nanostores/persistent

Ahora, vamos a crear un contador persistente que mantendrá su valor incluso cuando se recargue la página:

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

En este ejemplo, ‘persistentCount’ es la clave usada para almacenar el valor en localStorage, y 0 es el valor inicial.

Implementemos un contador persistente usando componentes de diferentes marcos. Este contador mantendrá su valor a través de recargas de página y estará accesible desde cualquier marco.

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

Ahora, puedes usar estos componentes juntos en una página de 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>

Este conjunto muestra un contador persistente en el que:

  • React maneja el incremento

  • Vue maneja el decremento

  • Svelte muestra el conteo actual

  • Astro proporciona un botón de reiniciar

El valor del contador persistirá entre recargas de página, mostrando el poder de @nanostores/persistent para mantener el estado.

El estado persistente es particularmente útil para:

  1. Preferencias de usuario (p. ej., ajustes de tema, opciones de idioma)

  2. Formularios parcialmente completados (para evitar la pérdida de datos por recarga accidental de página)

  3. Token de autenticación (para mantener las sesiones de usuario)

  4. Caché local de datos accedidos con frecuencia

Al aprovechar @nanostores/persistent, puedes mejorar la experiencia del usuario manteniendo datos de estado importantes entre cargas de página y sesiones del navegador.

Al integrar Nanostores en tus proyectos Astro, tienes en mente estas mejores prácticas y consejos para aprovechar al máximo esta solución de gestión de estado ligera.

  • Use atom para estados simples de valor único.

  • Utilice map para estados tipo objeto con múltiples propiedades.

  • Utilice computed para estados derivados que dependen de otros almacenes.

  • Utilice persistentAtom o persistentMap cuando necesite que el estado persista a través de recargas de página.

En lugar de crear almacenes grandes y monolíticos, prefiera almacenes pequeños y enfocados. Esta aproximación mejora la mantenibilidad y el rendimiento permitiendo actualizaciones más 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' })

Cuando tenga estados que dependan de otras partes de estado, use almacenes computados. Esto ayuda a mantener su estado DRY (Don’t Repeat Yourself) y asegura que el estado derivado siempre esté actualizado.

import { atom, computed } from 'nanostores'

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

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

Nanostores ofrece excelente soporte para TypeScript. Utilícelo para atrapar errores temprano y mejorar la experiencia del desarrollador.

import { atom } from 'nanostores'

interface User {
  id: number
  name: string
}

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

Aunque Nanostores es ligero, tenga en mente el rendimiento en aplicaciones más grandes. Use la función batched para agrupar múltiples actualizaciones de almacén juntas, reduciendo el número de re-renderizaciones.

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

Cuando uses Nanostores en un proyecto Astro con varios frameworks, intenta mantener la lógica central del estado independiente del framework. Esto facilita compartir el estado entre diferentes componentes de frameworks.

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

Aunque las tiendas persistentes son poderosas, úsalas con precaución. No todo el estado necesita persistir entre sesiones. El uso excesivo de tiendas persistentes puede generar comportamientos inesperados y posibles problemas de rendimiento.

Para facilitar la depuración, puedes usar la función onMount para registrar los cambios 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)
    })
  })
}

Cuando utilices Nanostores en componentes que puedan desmontarse, asegúrate de limpiar las suscripciones para evitar pérdidas de 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
}

Siguiendo estas mejores prácticas y consejos, podrás gestionar eficazmente el estado en tus proyectos Astro usando Nanostores, sin importar los frameworks que estés integrando.

Como hemos explorado a lo largo de este artículo, Nanostores ofrece una solución poderosa pero ligera para la gestión de estado en proyectos Astro, especialmente cuando se trabaja con múltiples frameworks. Recapitulemos los puntos clave:

  1. Versatilidad: Nanostores se integra perfectamente con Astro, React, Vue, Svelte y Solid, lo que lo convierte en una opción ideal para proyectos con múltiples frameworks.

  2. Simplicidad

    : Con su API directo, Nanostores ofrece una pendiente de aprendizaje baja mientras que aún proporciona capabilidades robustas de gestión de estado.

  3. Flexibilidad: Desde almacenes atómicos simples hasta estados computados complejos e incluso almacenamiento persistente, Nanostores se adapta a una amplia gama de necesidades de gestión de estado.
  4. Rendimiento: Su naturaleza ligera garantiza que Nanostores no inflará su aplicación, manteniendo los beneficios de rendimiento de Astro.
  5. Mejores Prácticas: Al seguir las guías que hemos discutido, como mantener los almacenes pequeños y enfocados, aprovechar TypeScript y usar almacenes computados para estados derivados, puedes crear sistemas de gestión de estado eficientes y maintainables.

Nanostores destaca en la arquitectura de islas de componentes de Astro, permitiéndole administrar el estado a través de componentes interactivos aislados de manera eficiente. Tanto si estás construyendo una página web simple con algunos elementos interactivos como si estás trabajando en una aplicación web compleja con múltiples frameworks, Nanostores proporciona las herramientas necesarias para manejar el estado de manera efectiva.

Mientras continúas tu viaje con Astro y Nanostores, recuerda que la mejor manera de aprender es haciendo. Experimenta con diferentes tipos de tiendas, intenta implementar estado compartido entre frameworks y explora las posibilidades de almacenamiento persistente. Cada proyecto traerá nuevos retos y oportunidades para refinar tus habilidades en el manejo del estado.

Este atento al próximo artículo de nuestra serie “Nanostores en Astro: una aventura con multiples frameworks”, donde profundizaremos en aplicaciones prácticas y técnicas avanzadas para el manejo de estado en proyectos de Astro.

Para profundizar tu comprensión de Nanostores y su uso en proyectos de Astro, revisa estos recursos valiosos:

¡Buen código y que tus proyectos Astro sean siempre estados y de alto rendimiento!

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