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

Three Mechanisms, Three Situations Duplicated props cost the payload twice and suit small constant values. A custom event on a common ancestor costs nothing in payload and suits occasional notifications between islands. A shared store costs one module in both bundles and a seeding script, and is the only option when several islands read and write a value continuously. Pick by whether the value changes, and how often duplicate the props — for small constants cost: the payload twice · benefit: no runtime, no module, nothing to synchronise custom event on a common ancestor — for occasional notifications cost: zero payload · benefit: no shared module; islands stay independent shared store — for continuous read and write cost: a module in both bundles plus a seed script · benefit: one source of truth

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

What Each Choice Costs on One Page For a small constant, duplication adds a negligible amount to the document and nothing to the bundles. The event approach adds nothing to either. The store adds a small module to both island bundles plus a seed script in the document, which only pays off when the value is genuinely shared state rather than a constant. MECHANISM DOCUMENT BUNDLES duplicated props (100 B value) +200 B unchanged custom event unchanged a few lines each shared store +seed script +store module, once if externalised

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.

Write It Down Beside the Islands A one-line comment naming the sharing channel turns an invisible coupling into something the next author can see, which matters because islands are otherwise independently replaceable. Document the coupling, because it is the only one reads basketCount from the shared store seeded inline in the layout, before any island module loads listens for variant:selected on the product container event name is the contract; neither island imports the other without the note, the coupling is discovered by breaking it

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.

← Back to Astro Islands and Client Directives