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
- Agnostic: Works with React, Vue, Svelte, Solid, Vanilla JS, and Astro.
- Lightweight: Doesn’t add unnecessary weight to your bundle.
- 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);
}
}
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>
);
}
- To read the state in React we use the
useStorehook. - 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>
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>
When you click the button rendered by React:
- React will execute
addCartItem. - Nano Store will update the value in memory and emit a change event.
- 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>
As we can see, it’s also possible and very simple to do.
