wandres.dev
VALTIO Y NANOSTORES · proxy y agnóstico

Nanostores: el store diminuto y agnóstico

Zustand, Jotai y Valtio nacieron dentro de React y arrastran su ciclo de vida. Nanostores nace fuera: menos de un kilobyte, sin dependencia de ningún framework, con adaptadores finos para React, Vue, Svelte, Solid y Angular. Esta lección explica por qué ese diseño importa justo ahora, cómo resuelve el problema que la arquitectura de islas de Astro plantea sin solución nativa, qué significa que los stores sean árboles agitables y perezosos, y en qué escenarios un store agnóstico es la respuesta correcta y no una excentricidad de tamaño.

⏱ 18 min

Todo lo que este track ha estudiado hasta aquí comparte un supuesto tan interiorizado que cuesta verlo: que la aplicación es un único árbol de componentes de un único framework. Redux vive en un proveedor de React, Zustand expone un hook, Jotai necesita su contexto, Valtio sirve instantáneas a un render. Cambia ese supuesto —imagina una página con tres islas interactivas escritas en tres frameworks distintos, cada una montada por separado sobre HTML estático— y todas esas herramientas se vuelven incómodas o directamente inaplicables. Nanostores es la respuesta a ese escenario: un store que no sabe nada de ningún framework, que pesa menos que la mayoría de los iconos de tu interfaz y que existe fuera del árbol porque nunca estuvo dentro. Esta lección explica por qué esa independencia deja de ser una curiosidad y se vuelve necesaria.

🎯 Al terminar esta lección sabrás
  • Situar el problema que la arquitectura de islas plantea al estado compartido y por qué los stores acoplados a un framework no lo resuelven.
  • Entender el diseño de nanostores: tamaño mínimo, núcleo agnóstico y adaptadores finos por framework.
  • Explicar qué significa que los stores sean perezosos y agitables, y qué consecuencias tiene en el tamaño del paquete.
  • Reconocer los escenarios donde un store agnóstico es la elección correcta y aquellos donde no aporta nada.

El problema que las islas plantean

En una aplicación de página única el estado compartido es fácil de razonar porque existe un único punto de montaje y un único árbol. En una arquitectura de islas ese punto no existe. Astro renderiza HTML en el servidor y salpica la página con componentes interactivos independientes que se hidratan por separado, cada uno con su propia raíz, sin ancestro común en tiempo de ejecución. Dos islas de React en la misma página no comparten contexto: son dos aplicaciones distintas que casualmente conviven en el mismo documento.

La consecuencia es inmediata y desagradable. Un carrito de compra cuyo contador vive en la cabecera y cuyo botón de añadir vive en la ficha de producto no puede comunicarse por contexto, porque no hay proveedor que envuelva a ambos. Poner un proveedor en cada isla crea dos estados independientes que divergen. Y montar toda la página como una única aplicación cliente derrota el propósito de haber elegido islas, que era enviar el mínimo de JavaScript posible.

ℹ️
La isla no es un componente, es una aplicación

Conviene interiorizar bien qué es una isla para no razonar mal sobre ella. No es un componente dentro de un árbol mayor: es una raíz de hidratación independiente, con su propio ciclo de montaje, que puede además cargarse de forma diferida según visibilidad o interacción. Dos islas pueden hidratarse con segundos de diferencia, o una puede no hidratarse nunca si el usuario no llega a verla. Cualquier solución de estado compartido en este contexto debe tolerar que sus consumidores aparezcan en momentos impredecibles y en cualquier orden.

Conviene notar que este no es un problema exótico de una herramienta concreta. Reaparece idéntico en micro frontends, en widgets incrustados en páginas ajenas y en cualquier migración gradual donde dos frameworks conviven durante meses. El árbol único era una condición del entorno, no una ley, y varias corrientes de la última década la han roto a la vez por motivos distintos.

La solución que el problema pide es un estado que viva fuera de todos los árboles, en el módulo, y al que cada isla se suscriba cuando le toque existir. Eso es exactamente un store agnóstico, y esa es la razón por la que la documentación de Astro recomienda nanostores en vez de las alternativas populares. No es una preferencia estética: es la única forma en que las piezas encajan.

flowchart TD
H[HTML estatico del servidor] --> I1[isla cabecera en React]
H --> I2[isla ficha en Svelte]
H --> I3[isla panel en Vue]
S[store en el modulo] --- I1
S --- I2
S --- I3
style S fill:#f9e2af,color:#11111b
style I1 fill:#89b4fa,color:#11111b
style I2 fill:#a6e3a1,color:#11111b
style I3 fill:#cba6f7,color:#11111b

Un núcleo que no sabe de vistas

El diseño de nanostores empieza por una decisión de alcance: el paquete central no importa ningún framework y no expone ningún hook. Ofrece stores con tres operaciones —leer, escribir y suscribirse— y nada más. Todo lo específico de una vista vive en paquetes separados de adaptación, tan finos que apenas son un envoltorio sobre la suscripción.

import { atom } from 'nanostores'

export const contador = atom(0)

contador.get()
contador.set(contador.get() + 1)

const cancelar = contador.subscribe((valor) => {
  console.log('nuevo valor', valor)
})

Ese fragmento no menciona React, ni Vue, ni Astro, y funciona igual en un módulo de servidor, en un trabajador web o en una prueba unitaria sin entorno de navegador. La adaptación a cada framework se importa aparte y consiste en poco más que conectar la suscripción al mecanismo de reactividad del anfitrión.

import { useStore } from '@nanostores/react'
import { contador } from './store'

function Contador() {
  const valor = useStore(contador)
  return <button onClick={() => contador.set(valor + 1)}>{valor}</button>
}
🪶

Menos de un kilobyte

El nucleo cabe en un puñado de bytes comprimidos. En una pagina donde el objetivo es enviar casi nada de JavaScript, ese orden de magnitud decide.

🔌

Adaptadores finos

Paquetes separados para React, Vue, Svelte, Solid, Preact y Angular. Cada uno traduce la suscripcion al idioma reactivo del anfitrion.

🌱

Stores perezosos

Un store sin oyentes no calcula ni retiene nada. Se activa con el primer suscriptor y se apaga cuando el ultimo se va.

🌲

Agitable por diseño

Cada store es una exportacion suelta, asi que el empaquetador elimina los que ninguna pagina usa. El coste es proporcional al uso real.

Pereza, ciclo de vida y coste proporcional

Dos rasgos del diseño merecen ser entendidos y no solo enumerados, porque cambian cómo se escribe el código. El primero es la pereza. Un store de nanostores permanece inerte mientras nadie lo escucha: no ejecuta su lógica de inicialización, no abre conexiones, no calcula derivados. En cuanto llega el primer suscriptor se monta, y cuando el último se va, tras un breve margen, se desmonta y libera lo que tuviera abierto. Ese ciclo de vida está expuesto y es utilizable.

import { atom, onMount } from 'nanostores'

export const reloj = atom(Date.now())

onMount(reloj, () => {
  const id = setInterval(() => reloj.set(Date.now()), 1000)
  return () => clearInterval(id)
})

El intervalo no existe hasta que alguna isla se suscribe al reloj, y desaparece cuando la última se desmonta. En una página con islas que se hidratan según visibilidad, ese comportamiento es exactamente lo que hace falta: los recursos siguen a los consumidores en vez de existir por si acaso. Compáralo con un store global tradicional, que se inicializa al importar el módulo y mantiene vivo lo que abrió hasta que se descarga la página.

La pereza tiene también una lectura arquitectónica que va más allá del ahorro. Un store que se monta con su primer oyente y se desmonta con el último es, en la práctica, un recurso con ciclo de vida propio, y eso permite colocar dentro del módulo de estado cosas que normalmente acaban dispersas por los efectos de los componentes: una conexión persistente, un temporizador, un observador del navegador. La ventaja no es estética. Cuando ese recurso vive en el store, su apertura y su cierre están escritos en un solo sitio y no dependen de que cada consumidor recuerde limpiar lo suyo, que es el origen habitual de las fugas.

El segundo rasgo es el coste proporcional. Como cada store es una exportación independiente y no un miembro de un objeto grande, el análisis estático del empaquetador puede eliminar los que una página concreta no toca. En una aplicación de página única eso importa poco, porque todo acaba en el mismo paquete; en un sitio con decenas de páginas y unas pocas islas por página, importa mucho, porque cada página paga solo el estado que usa.

💡
El store como módulo, no como aplicación

La forma idiomática de organizar nanostores es un archivo por dominio que exporta sus stores y sus funciones de mutación, sin ninguna importación de framework. Ese archivo es consumible desde una isla de React, desde una de Svelte, desde un script suelto del navegador o desde una prueba en Node. Cuando el estado deja de estar acoplado a la vista, empieza a comportarse como lo que siempre debió ser: una capa de dominio, con su propia superficie pública y su propio conjunto de pruebas.

Dónde encaja y dónde no

La honestidad exige delimitar. Nanostores gana con claridad en tres escenarios. El primero es el que motivó su diseño: sitios con islas, sea Astro, Eleventy con hidratación parcial o cualquier montaje múltiple. El segundo es el de aplicaciones que conviven con varios frameworks a la vez, un caso frecuente en migraciones graduales y en micro frontends, donde tener el estado en terreno neutral evita duplicarlo. El tercero es el de bibliotecas y componentes distribuibles que necesitan estado propio y no pueden imponer un framework a quien las consume.

Hay un cuarto escenario menos obvio y cada vez más frecuente: el código que debe correr igual en el servidor y en el cliente. Como el núcleo no depende de ningún framework ni de ninguna API del navegador, un módulo de stores se importa sin ceremonia desde una función de servidor, desde una tarea de compilación o desde una prueba en Node. Eso convierte al store en un lugar razonable para alojar lógica de dominio compartida entre ambos lados, algo que un store atado al ciclo de vida de React nunca pudo ser sin envoltorios.

// mismo modulo importable desde una isla, desde el servidor o desde una prueba
import { atom, computed } from 'nanostores'

export const lineas = atom<Array<{ precio: number; cantidad: number }>>([])
export const unidades = computed(lineas, (items) =>
  items.reduce((suma, l) => suma + l.cantidad, 0),
)

Fuera de esos escenarios el cálculo cambia. En una aplicación de React de página única, Zustand ofrece una ergonomía mejor y un ecosistema más grande a cambio de dos kilobytes más, y elegir nanostores por el tamaño sería optimizar la métrica equivocada. Para grafos de derivación densos, Jotai modela mejor. Para escritura imperativa y anidada, Valtio es más natural. La independencia de framework es una virtud cara de aprovechar si nadie va a usarla.

⚠️
El estado compartido entre islas no es estado del servidor

Un error que aparece pronto en proyectos con Astro es usar el store para guardar datos que vinieron del servidor y que el HTML ya había renderizado. Eso duplica la verdad y reintroduce el problema de sincronización que la renderización en servidor había eliminado. El store entre islas es para estado propio del cliente que varias islas comparten: el carrito, el tema, la sesión, un filtro. Lo que el servidor ya sabe debe llegar por propiedades a la isla o volver a pedirse, no vivir dos veces.

El acoplamiento al framework era una comodidad, no una necesidad

Durante una década el estado de una aplicación web vivió dentro del framework, y ese hecho se volvió tan natural que dejó de percibirse como una decisión. Un store era algo que se registraba en un proveedor, se leía con un hook y moría con el árbol. Pero nada del problema del estado compartido exige eso: el estado es un valor que cambia en el tiempo y al que varias partes quieren reaccionar, una descripción que no menciona componentes, ni render, ni ciclos de vida. Lo que ocurrió fue que el framework era el único sitio con un mecanismo de suscripción ya construido, y aprovecharlo era gratis. El acoplamiento fue conveniencia disfrazada de arquitectura. Cuando la arquitectura de islas eliminó el árbol único, esa conveniencia se convirtió en un impedimento, y nanostores responde con lo mínimo necesario: un valor, unos oyentes y un ciclo de vida. La comparación con lo que ocurrió en el estado del servidor es exacta y vale la pena mirarla de frente. Cuando TanStack Query se llevó la cache remota fuera del store global, no añadió una capacidad nueva: reconoció que aquello nunca había pertenecido allí y le dio su propio hogar. Sacar el estado del cliente fuera del framework es el mismo movimiento aplicado un piso más abajo, y produce el mismo efecto: cada cosa en el sitio donde sus problemas propios se pueden resolver bien. El corolario para el ingeniero es un hábito de sospecha productiva. Ante cualquier pieza de tu sistema que viva dentro de otra, pregunta si está ahí porque su naturaleza lo exige o porque era el sitio donde había herramientas a mano. La segunda respuesta es la habitual, y casi siempre señala el próximo desacoplamiento que tu arquitectura agradecerá. Un kilobyte que no sabe qué framework usas no es una hazaña de minimalismo: es la demostración de que casi todo el peso que aceptábamos era el precio de una dependencia que nunca fue necesaria.

⚔️ Comparte estado entre islas de frameworks distintos
  1. Monta una página de Astro con dos islas, una en React y otra en Svelte o Vue, y comprueba primero que un contexto de React no puede unirlas.
  2. Crea un módulo con un atom de nanostores sin ninguna importación de framework y consúmelo desde ambas islas con sus adaptadores. Verifica que se sincronizan.
  3. Usa onMount para abrir un recurso —un intervalo o una conexión— y observa en las herramientas del navegador que no existe hasta que la primera isla se hidrata.
  4. Configura una isla para hidratarse solo al hacerse visible y confirma que el store se monta al aparecer y se desmonta al quedarse sin oyentes.
  5. Mide el paquete de dos páginas que usan stores distintos y comprueba que cada una solo incluye el suyo. Explica el resultado en términos de exportaciones agitables.
  6. Escribe una prueba unitaria del módulo de store en Node, sin entorno de navegador ni framework. Si necesitas montar algo para probarlo, el acoplamiento sigue ahí.