Sharing Props Between Astro Islands Without a Store
Two Astro islands that need the same value have three options, and the usual instinct — reach for a store — is often the most expensive. This walkthrough works through duplication, DOM events and stores with the payload cost of each, extending the directive material in Astro islands and client directives.
Prerequisites
Implementation Steps
Step 1 — Duplicate, when the value is a small constant
---
// Both islands need the product id and its variant labels. Neither mutates them.
const projected = { id: product.id, variants: product.variants.map((v) => v.label) };
---
Two copies of a hundred-byte object is two hundred bytes. Introducing a store to save one hundred bytes costs a module in both bundles and a subscription in both components — a clear loss.
Step 2 — Use a DOM event when one island needs to tell another something
// In the picker island: announce, do not reach.
picker.addEventListener('change', () => {
picker.dispatchEvent(new CustomEvent('variant:selected', {
bubbles: true, // reaches any ancestor listener
detail: { variantId: select.value },
}));
});
// In the stock notice island: listen on a common ancestor, not on the picker.
document.querySelector('[data-product]').addEventListener('variant:selected', (e) => {
refresh(e.detail.variantId); // no import, no coupling
});
Neither island imports the other and neither needs a store. The coupling is a documented event name, which is looser than a shared module and survives one island being replaced entirely.
Step 3 — Use a store when several islands read and write continuously
// src/stores/basket.ts — externalised so both bundles share one instance.
import { atom } from 'nanostores';
// Seeded from an inline script the server wrote, so the first render is correct.
export const basketCount = atom(window.__BASKET_COUNT__ ?? 0);
<script is:inline set:html={`window.__BASKET_COUNT__ = ${JSON.stringify(count)};`} />
The seeding step is what makes this competitive with duplicated props on a slow connection: without it, every island renders a default first and corrects itself, which is visible. Details of the store approach itself are in using nanostores for cross-island state.
Verification
Two checks confirm the implementation, whichever mechanism you chose.
One instance, if you used a store. Search the built bundles for the store module. It must appear once across the whole graph; appearing in each island’s chunk means the two islands hold separate atoms and the sharing does not exist, which the page will demonstrate the first time one writes and the other does not update.
Correct first paint. Load with scripting disabled. Both islands must render the correct server value — from props in the duplication case, from the server-rendered markup in the store case. A default value visible here means the seeding step is missing and every visitor on a slow connection sees the same flash.
The Case for Duplication
There is a reflex in front-end work that treats duplicated data as a defect regardless of size. In an islands architecture that reflex is often wrong, because the alternative has costs the reflex does not account for.
A store is a runtime dependency shared between components that would otherwise be independent. It has to be externalised correctly or it silently duplicates. It needs seeding or it flashes. It creates an ordering requirement between islands that a props payload does not. And it makes each island harder to reason about alone, because its state now comes from two directions.
Duplicated props have exactly one cost — bytes — and that cost is measurable in seconds. When the measurement says two hundred bytes, take the simpler design. When it says four kilobytes on a page with three islands, or when a write is involved, take the store. Making that decision with a number rather than a principle is the whole recommendation, and it is the same reasoning applied to boundary size in estimating the cost of a hydration boundary.
One practical addition: whichever mechanism you choose, write it down beside the islands that use it. A comment naming the channel — “reads basketCount from the shared store, seeded inline in Layout.astro” — saves the next person from discovering the coupling by breaking it. Islands are designed to be independently replaceable, and the sharing mechanism is the one place where that independence is qualified, so it deserves an explicit note rather than an inference from imports.
Troubleshooting
Is duplicating props between two islands actually a problem?
Only when the payload is large or the value changes. Two islands each receiving a forty-byte product id is fine and simpler than any store. Two islands each receiving a two-kilobyte object is four kilobytes in every document for every visitor, which is worth avoiding. And any value that one island mutates cannot be duplicated at all, because the second copy will not follow.
Why not just use a store for everything?
Because a store is a module both islands import, which means it is bundled into both entry points unless it is externalised, and because it adds a subscription lifecycle to components that may only need a constant. For values that never change, props are smaller, simpler and impossible to get out of sync. Reach for a store when there is a write.
How do I seed a store before any island activates?
Write the initial value into an inline script in the document, before the island modules load, and have the store read it on first import. That way the first render of every island shows server data rather than a default, and there is no flash of an empty value on slow connections. Without seeding, a store-based approach is visibly worse than duplicated props on exactly the connections that need help most.
Related
- Using Nanostores for Cross-Island State — the store mechanism in full, including computed values.
- Astro Islands and Client Directives — directives, triggers and the boundary these props cross.
- Cross-Boundary Prop Passing Patterns — what may be serialised into props at all.
← Back to Astro Islands and Client Directives