astro-estado-compartido-nanostores

Sharing State Between Islands with Nano Stores

  • 5 min

Astro’s “Islands” model is fantastic for performance, but it presents a significant architectural challenge.

If I have a Buy Button in React in the header and a Cart in Svelte in the sidebar… how do I make the cart update when the button is clicked?

In a traditional application (Next.js, CRA), we would wrap everything in a CartContext. In Astro we cannot do this, because each island hydrates independently. They do not share a common ancestor in the virtual DOM.

The solution is to use a state management library that is framework-agnostic. Astro’s official recommendation is Nano Stores.

What is Nano Stores?

Nano Stores is a tiny state management library (less than 1KB) designed to move easily between frameworks.

Advantages of Nano Stores

  1. Agnostic: Works with React, Vue, Svelte, Solid, Vanilla JS, and Astro.
  2. Lightweight: Doesn’t add unnecessary weight to your bundle.
  3. No dependencies: Doesn’t require complex configurations.

Installation

To use it, we need to install the core and, optionally, the adapters for the frameworks we are using.

npm install nanostores @nanostores/react @nanostores/vue @nanostores/svelte

How to use Nano Stores with Astro

Let’s see how to use Nano Stores with Astro using an example where we will emit an event from a React component and receive it in a Svelte component.

Step 1: Create the Store

The first step is to define our state. Unlike Redux, we don’t need a huge central file. We can create small .ts or .js files wherever we need them.

Let’s create a file to manage a simple shopping cart.

// src/stores/cartStore.ts
import { atom, map } from 'nanostores';

// 'atom' is used for simple values (strings, numbers, booleans)
export const isCartOpen = atom(false);

// 'map' is used for objects or dictionaries
export const cartItems = map<Record<string, number>>({});

/**
 * Helper function to add items
 * It's good practice to define the modification logic here,
 * outside of the UI components.
 */
export function addCartItem(id: string) {
  const existingEntry = cartItems.get()[id];
  if (existingEntry) {
    cartItems.setKey(id, existingEntry + 1);
  } else {
    cartItems.setKey(id, 1);
  }
}
Copied!

Notice that this file is pure TypeScript/JavaScript. It doesn’t import React, Vue, or anything. This allows any part of your application (even inline scripts) to read from or write to it.

Step 2: The emitter (React)

Let’s create a React component that adds products to the cart. This component will write to the store.

// src/components/AddToCartButton.tsx
import { useStore } from '@nanostores/react';
import { addCartItem, isCartOpen } from '../stores/cartStore';

export default function AddToCartButton({ productId }: { productId: string }) {
  // We read the state (optional, only if we need to react to changes)
  const $isOpen = useStore(isCartOpen);

  return (
    <button
      className="bg-blue-600 text-white px-4 py-2 rounded"
      onClick={() => {
        // 1. Execute the business logic
        addCartItem(productId);
        
        // 2. Modify the atom state directly
        isCartOpen.set(true); 
      }}
    >
      Add to Cart (Panel is {$isOpen ? 'Open' : 'Closed'})
    </button>
  );
}
Copied!
  • To read the state in React we use the useStore hook.
  • To write the state, we use the atom’s .set() method or call our helper function.

Step 3: The receiver (Svelte)

Now let’s create the cart display using Svelte. Svelte has great native integration with the observable pattern, so using Nano Stores is incredibly clean.

<script>
  // In Svelte we import the store directly
  import { isCartOpen, cartItems } from '../stores/cartStore';
</script>

{#if $isCartOpen}
  <div class="cart-panel">
    <h3>Your Cart</h3>
    <ul>
      {#each Object.entries($cartItems) as [id, qty]}
        <li>Product {id}: <strong>{qty}</strong></li>
      {/each}
    </ul>
    
    <button on:click={() => isCartOpen.set(false)}>Close</button>
  </div>
{/if}

<style>
  .cart-panel {
    border: 1px solid #ccc;
    padding: 1rem;
    background: #f9f9f9;
    position: fixed;
    top: 20px;
    right: 20px;
  }
</style>
Copied!

See how clean Svelte is? Simply by using $isCartOpen, Svelte handles everything.

If you come from React, this will seem like magic, but it’s standard in Svelte 😉.

Step 4: Putting it all together in Astro

Finally, we combine our islands on a page.

---
// src/pages/tienda.astro
import Layout from '../layouts/Layout.astro';
import AddToCartButton from '../components/AddToCartButton';
import CartFlyout from '../components/CartFlyout.svelte';
---

<Layout title="Store">
  <h1>Shared State Example</h1>

  <div class="grid-productos">
    <AddToCartButton client:visible productId="prod-1" />
    <AddToCartButton client:visible productId="prod-2" />
  </div>

  <CartFlyout client:idle />

</Layout>
Copied!

When you click the button rendered by React:

  1. React will execute addCartItem.
  2. Nano Store will update the value in memory and emit a change event.
  3. The Svelte component (which is subscribed) will receive the notification and re-render instantly.

Usage with Vanilla JS

Sometimes you don’t need a UI framework. Sometimes you just want a vanilla JS <script> inside your .astro file that also reacts.

<script>
  import { isCartOpen } from '../stores/cartStore';

  // Subscribe to changes in vanilla JS
  isCartOpen.subscribe(open => {
    console.log('The cart state has changed to:', open);
    if(open) {
      document.body.classList.add('carrito-abierto');
    } else {
      document.body.classList.remove('carrito-abierto');
    }
  });
</script>
Copied!

As we can see, it’s also possible and very simple to do.