Sharing State Across Islands
The isolation problem
Section titled “The isolation problem”Islands hydrate independently — that’s what makes them cheap. But it has a consequence that trips people up coming from a SPA: two islands do not share state automatically. A React useState in one island is invisible to another island, even one made from the same framework. There is no shared app tree; each island is its own little root.
So how do a “add to cart” island and a “cart count” island in the header stay in sync? You have three tools, from simplest to most powerful.
1. Pass serializable props from the server
Section titled “1. Pass serializable props from the server”If the data is known when the page renders, the simplest answer is to compute it once in the .astro file and hand it to each island as a prop. No cross-island coordination needed — the server is the shared source.
---import CartBadge from '../components/CartBadge.jsx';const initialCount = await getCartCount(Astro.request);---<CartBadge count={initialCount} client:load />This covers initial state. It doesn’t help when one island needs to update another at runtime — that’s what a store is for.
2. nanostores for cross-island shared state
Section titled “2. nanostores for cross-island shared state”When islands must react to each other at runtime, use a shared store that lives outside any single island. Astro’s docs recommend nanostores — it’s tiny, framework-agnostic, and has adapters for each framework (@nanostores/react, @nanostores/vue, …). You define the store once and any island can read and write it.
import { atom } from 'nanostores';export const cartCount = atom(0);// AddToCart.jsx (React island)import { cartCount } from '../stores/cart.js';export default function AddToCart() { return <button onClick={() => cartCount.set(cartCount.get() + 1)}>Add</button>;}// CartBadge.jsx (another React island, elsewhere on the page)import { useStore } from '@nanostores/react';import { cartCount } from '../stores/cart.js';export default function CartBadge() { const count = useStore(cartCount); // re-renders when the store changes return <span>Cart: {count}</span>;}Now clicking “Add” in one island updates the badge in a completely separate island — because both subscribe to the same store, not to each other.
flowchart TB store["nanostore: cartCount"] add["AddToCart island — writes"] --> store store --> badge["CartBadge island — reads and re-renders"]
3. Slots: pass server-rendered children into an island
Section titled “3. Slots: pass server-rendered children into an island”Sometimes you don’t need shared state — you need to put server-rendered content inside an interactive shell. A framework island can accept a <slot /> the same way an .astro component does, so an interactive <Tabs> island can wrap static, server-rendered panels. The shell is interactive; the content ships as HTML with no extra island JS.
---import Accordion from '../components/Accordion.jsx';---<Accordion client:visible> <p>This paragraph is server-rendered HTML, passed into the island via its slot.</p></Accordion>Choosing among the three
Section titled “Choosing among the three”- Data known at render time, no runtime updates → props from the server.
- Islands must update each other at runtime → a nanostore.
- You need interactive chrome around static content → a slot.
Reach for a store only when you genuinely have runtime coordination between separate islands. Most “shared state” needs turn out to be initial props or slots, which ship less JavaScript.