wandres.dev
INTEGRACIONES DE FRAMEWORK · React, Solid, Vue, Svelte

Compartir estado entre islas con nanostores

Las islas están aisladas por diseño: el contexto de React o el provide de Vue no cruzan de una isla a otra, y menos entre frameworks distintos. La solución idiomática en Astro es sacar el estado fuera de todos los frameworks a un almacén agnóstico —nanostores— con sus primitivas atom, map y computed, y conectar cada isla mediante su adaptador: useStore en React, Vue o Solid, y la suscripción con dólar en Svelte. Así una isla de Svelte y otra de React observan la misma fuente de verdad y se mantienen en sincronía, permitiendo mantener las islas pequeñas e hidratar solo lo imprescindible.

⏱ 17 min

Has llegado al problema que corona el nivel: tienes varias islas, quizá de frameworks distintos, y necesitas que compartan estado —un carrito, un tema, un usuario— de modo que tocar una actualice a las demás. El instinto de React sería un contexto; el de Vue, un provide. Ninguno sirve, porque esos mecanismos no cruzan la frontera de una isla, y mucho menos la frontera entre frameworks. La respuesta de Astro es más simple y más profunda: saca el estado fuera de todos los frameworks, a un almacén que no pertenece a ninguno, y deja que cada isla se asome a él.

🎯 Al terminar esta lección sabrás
  • Entender por qué el contexto de un framework no cruza de una isla a otra ni entre frameworks.
  • Modelar estado compartido con las primitivas atom, map y computed de nanostores.
  • Conectar islas de React, Vue, Solid y Svelte al mismo almacén con sus adaptadores.
  • Mantener las islas pequeñas e hidratar solo lo imprescindible apoyándote en el estado externo.

El problema: las islas están aisladas

Una isla es un árbol de componentes que se hidrata por su cuenta, con su propia instancia del framework. Dos islas de React en la misma página son dos árboles separados, cada uno con su propio contexto; un Context.Provider de una no envuelve a la otra, así que su estado no se comparte. Y si las islas son de frameworks distintos, la incomunicación es total: el sistema reactivo de Svelte y el de Vue no tienen ningún canal común. El aislamiento no es un defecto, es la esencia de las islas —cada una es una región interactiva independiente—, pero deja abierta la pregunta de cómo coordinar varias.

La tentación de guardar el estado en una variable de módulo compartida resuelve el transporte pero no la reactividad: una isla que muta esa variable no tiene forma de avisar a la otra de que vuelva a pintarse. Falta el ingrediente que convierte un valor compartido en un estado observable: un mecanismo de suscripción que notifique a quien mire cuando el valor cambie. Eso es exactamente lo que aporta un almacén como nanostores.

nanostores: estado fuera de todos los frameworks

nanostores es una librería diminuta —del orden de cientos de bytes— cuya única misión es guardar estado observable fuera de cualquier framework. Su núcleo son tres primitivas. Un atom guarda un valor único; un map, un objeto de varias claves; y un computed deriva un valor de otros almacenes y se recalcula solo cuando ellos cambian. Todos exponen el mismo contrato mínimo: get para leer, set para escribir y subscribe para enterarse de los cambios.

// src/stores/carrito.ts
import { atom, computed } from 'nanostores';

export const items = atom<string[]>([]);
export const total = computed(items, (lista) => lista.length);

export function anadir(id: string) {
  items.set([...items.get(), id]);
}

Lo esencial de este fichero es dónde vive: en un módulo .ts normal, sin una sola línea de React, Vue o Svelte. El estado no está atrapado dentro del árbol de ningún componente; existe en un plano neutral que cualquier isla puede importar. total es un computed que se mantiene en sincronía con items sin que nadie lo orqueste, y anadir es la única puerta por la que se muta el carrito. Ese módulo es la fuente de verdad; las islas serán meras vistas asomadas a él.

Cuando el estado es un objeto de varios campos independientes —un formulario, unas preferencias—, map encaja mejor que un atom: notifica los cambios clave por clave, de modo que una isla suscrita a un campo no se repinta cuando cambia otro. Y los computed se encadenan: puedes derivar de un map y, de ese computed, derivar otro, tejiendo un pequeño grafo de estado cuya única raíz son las escrituras que haces con set. El resto —propagación, recálculo, notificación— lo resuelve el almacén sin que ninguna isla intervenga.

Un adaptador por framework, una misma fuente

Cada framework se conecta al almacén con un adaptador oficial que traduce la suscripción de nanostores a su propio sistema de reactividad. En React, Preact, Vue y Solid el puente es un hook llamado useStore; en Svelte, el propio contrato de store del framework permite suscribirse con el prefijo de dólar, sin hook.

// Badge.jsx de React
import { useStore } from '@nanostores/react';
import { total } from '../stores/carrito';

export default function Badge() {
  const n = useStore(total);
  return <span class="badge">{n} en el carrito</span>;
}
<!-- Boton.svelte -->
<script>
  import { items, anadir } from '../stores/carrito';
</script>

<button on:click={() => anadir('cafe')}>Añadir café</button>
<p>{$items.length} productos</p>

El useStore(total) de React devuelve el valor actual y vuelve a renderizar el Badge cuando cambia; el $items de Svelte se suscribe automáticamente y repinta al mutar. Ahora colócalos en la misma página desde una plantilla .astro, cada uno como isla independiente.

---
import Boton from '../components/Boton.svelte';
import Badge from '../components/Badge.jsx';
---
<Boton client:load />
<Badge client:visible />

Cuando pulsas el botón de Svelte, anadir muta el atom; el computed recalcula total; el adaptador de React lo notifica al Badge, que se repinta con el número nuevo. Una isla de Svelte acaba de actualizar una isla de React sin que ninguna sepa de la existencia de la otra, porque ambas solo conocen el almacén. Ese es el mecanismo entero, y funciona entre cualquier combinación de frameworks porque el estado vive por debajo de todos ellos.

Merece la pena ver por qué esto funciona y un contexto de React no. El almacén no es reactivo para un framework concreto; es reactivo en abstracto, con un contrato de suscripción que cualquier adaptador puede consumir. El de React traduce ese subscribe a un re-render, el de Svelte al mecanismo de store nativo del lenguaje, el de Vue a una ref. Cada adaptador es un cable finísimo entre un protocolo neutral y la reactividad de un framework, y como todos se enchufan al mismo protocolo, todos perciben el mismo latido. El contexto de React fracasa aquí por lo contrario: es reactivo solo dentro de su propio árbol, y una isla es un árbol distinto.

⚛️

atom

Guarda un valor único observable. get para leer, set para escribir, subscribe para reaccionar. La primitiva base de todo el modelo.

🗺️

map

Guarda un objeto de varias claves y notifica cambios por clave. Ideal para un formulario o un perfil con campos independientes.

🧮

computed

Deriva un valor de otros almacenes y se recalcula solo cuando ellos cambian. Estado derivado sin duplicar la fuente.

flowchart TD
STORE[almacen nanostores fuera de los frameworks] --> R[isla react con useStore]
STORE --> V[isla vue con useStore]
STORE --> S[isla svelte con dolar]
R -.muta.-> STORE
V -.muta.-> STORE
S -.muta.-> STORE
STORE --> SYNC[todas las islas en sincronia]
style STORE fill:#89b4fa,color:#11111b
style SYNC fill:#a6e3a1,color:#11111b

Hidratar solo lo imprescindible

El estado externo no solo resuelve la comunicación: cambia cómo diseñas las islas. Sin él, la tentación es agrandar una isla para que abarque todo lo que comparte estado, arrastrando a la interactividad —y al coste de JavaScript— zonas que solo necesitan leer un dato. Con un almacén compartido puedes hacer justo lo contrario: fragmentar la interfaz en islas mínimas, cada una suscrita únicamente a la porción del estado que le concierne, y darle a cada una la directiva más perezosa que admita.

---
import Total from '../components/Total.jsx';       // solo lee el total
import Acciones from '../components/Acciones.svelte'; // muta el carrito
---
<Acciones client:idle />
<p>Tu compra suma <Total client:visible /> artículos.</p>

Aquí Acciones se hidrata cuando el navegador queda ocioso y Total solo cuando entra en pantalla; entre medias, todo el resto de la página es HTML estático. Ninguna de las dos islas necesita envolver a la otra ni conocerla: ambas se coordinan a través del almacén. El resultado es un sitio donde el JavaScript crece por porciones pequeñas y justificadas, en lugar de por una gran isla que hidrata de golpe zonas que podrían haber seguido siendo texto.

💡
Persistencia y estado derivado casi gratis

Como el almacén vive fuera de los frameworks, ampliarlo no toca ninguna isla. Con @nanostores/persistent un atom se sincroniza solo con localStorage, de modo que el carrito o el tema sobreviven a una recarga sin una línea de código en tus componentes. Y cada nuevo computed añade estado derivado —un subtotal, un contador de errores— que todas las islas suscritas reciben al instante. La fuente de verdad es un punto único que puedes enriquecer sin propagar cambios por la interfaz.

📝
Reserva el almacén para estado de cliente compartido

nanostores brilla para el estado de interfaz que varias islas comparten en el navegador. Evita guardar en un módulo compartido datos propios de una petición concreta cuando renderizas bajo demanda, porque ese módulo puede persistir entre peticiones en el servidor y filtrar estado de un visitante a otro. La regla segura: los almacenes compartidos son para el estado que nace y vive en el cliente; los datos por petición viajan por props y por el render de servidor, no por un singleton de módulo.

El estado no pertenece a la interfaz que lo muestra

La lección más honda de nanostores en Astro no es una técnica, sino una inversión de dónde creemos que vive el estado. El modelo mental que heredamos de los frameworks de componentes dice que el estado pertenece a un componente: nace dentro de un árbol, se propaga hacia abajo por props y hacia arriba por eventos, y su hogar natural es la jerarquía de la interfaz que lo usa. Ese modelo es cómodo mientras hay un único árbol, pero se rompe en cuanto la interfaz se fragmenta en islas independientes, porque ya no existe un árbol común donde alojar lo compartido. La arquitectura de islas obliga entonces a reconocer una verdad que el árbol único ocultaba: el estado y la vista son cosas distintas, y confundirlas fue siempre una simplificación, no una ley. Al sacar el estado a un almacén externo, Astro lo devuelve a su lugar propio —un plano de datos que existe por sí mismo— y degrada las islas a lo que en el fondo siempre fueron: vistas, proyecciones observables de un estado que no les pertenece. Esta separación entre el estado y sus vistas es uno de los principios más fértiles del diseño de software, y aparece una y otra vez bajo nombres distintos: el modelo separado de la vista, el almacén único frente a componentes tontos, la fuente de verdad desacoplada de sus presentaciones. Lo que nanostores hace, casi con violencia por lo pequeño que es, es demostrar que ese principio no necesita un framework que lo imponga: basta un valor observable con una función de suscripción, doscientos bytes, para que múltiples vistas escritas en tecnologías incompatibles se mantengan coherentes sin conocerse. Y hay una consecuencia arquitectónica que trasciende el ejemplo. Cuando el estado vive fuera de las vistas, las vistas se vuelven baratas, desechables, intercambiables: puedes reescribir una isla en otro framework, partirla en dos, hidratarla más tarde, y el estado permanece intacto porque nunca dependió de ella. Has invertido la relación de poder habitual —ya no es la interfaz la que posee los datos, son los datos los que sobreviven a cualquier interfaz—, y esa inversión es exactamente lo que permite que un sitio de islas mínimas, cada una hidratada solo cuando hace falta, se comporte como una aplicación coherente. Dominar Astro es, al final, aprender a preguntar de cada dato no qué componente lo tiene, sino dónde debe vivir para que ningún componente lo necesite tener.

⚔️ Coordina islas de frameworks distintos
  1. Crea un almacén con un atom y un computed, más una función que lo mute, en un módulo .ts sin nada de framework.
  2. Escribe dos islas de frameworks distintos —una que mute y otra que solo lea— y conéctalas al almacén con sus adaptadores; verifica que actuar en una actualiza la otra.
  3. Da a cada isla una directiva client: distinta e inspecciona la red para confirmar que se hidratan por separado y aun así comparten estado.
  4. Añade persistencia con @nanostores/persistent, recarga la página y comprueba que el estado sobrevive sin haber tocado ninguna de las dos islas.