| name | lifecycle |
| description | Island lifecycle events, subtree helpers, and teardown. onIslandLoad and onIslandError helpers from vite-plugin-shopify-theme-islands/events — prefer these over raw document.addEventListener for guaranteed type safety. Raw DOM events islands:load and islands:error on document. islands:load detail includes tag, duration (ms), and attempt (1-based). islands:error detail includes tag, error, and attempt, including custom directive failures and directiveTimeout expiry. `./revive` now exports scan(), observe(), unobserve(), and disconnect(). disconnect() prevents init from ever starting if called early. Startup, DOM walking, mutation observation, and parent/child activation gating are now owned by src/lifecycle.ts, while subtree/runtime helper coordination is owned by src/runtime-ownership.ts. Event dispatch is via src/runtime-surface.ts; debug-only diagnostics live in src/runtime-observability.ts. Shopify section/block events bridged by default.
|
| metadata | {"type":"core","library":"vite-plugin-shopify-theme-islands","library_version":"2.0.1"} |
| sources | ["Rees1993/vite-plugin-shopify-theme-islands:src/events.ts","Rees1993/vite-plugin-shopify-theme-islands:src/runtime-ownership.ts","Rees1993/vite-plugin-shopify-theme-islands:src/runtime-observability.ts","Rees1993/vite-plugin-shopify-theme-islands:src/runtime-surface.ts","Rees1993/vite-plugin-shopify-theme-islands:src/lifecycle.ts","Rees1993/vite-plugin-shopify-theme-islands:src/retry-scheduler.ts","Rees1993/vite-plugin-shopify-theme-islands:src/cancellable-watchers.ts","Rees1993/vite-plugin-shopify-theme-islands:src/activation-session.ts","Rees1993/vite-plugin-shopify-theme-islands:src/shopify-lifecycle.ts","Rees1993/vite-plugin-shopify-theme-islands:src/contract.ts","Rees1993/vite-plugin-shopify-theme-islands:src/runtime.ts","Rees1993/vite-plugin-shopify-theme-islands:src/revive-module.ts"] |
Setup
import { onIslandLoad, onIslandError } from "vite-plugin-shopify-theme-islands/events";
const offLoad = onIslandLoad(({ tag, duration, attempt }) => {
console.log("loaded:", tag, `${duration.toFixed(1)}ms`, `attempt ${attempt}`);
});
const offError = onIslandError(({ tag, error, attempt }) => {
console.error("failed:", tag, `attempt ${attempt}`, error);
});
offLoad();
offError();
Core Patterns
Track island load for analytics
import { onIslandLoad } from "vite-plugin-shopify-theme-islands/events";
onIslandLoad(({ tag, duration, attempt }) => {
analytics.track("island_loaded", { component: tag, duration, attempt });
});
tag is the lowercased custom element tag name (e.g. "product-form"). duration is the time in milliseconds from when all directives resolved to when the module finished loading. attempt is 1 on the first successful load, 2 if it succeeded on the first retry, etc.
Track load performance
import { onIslandLoad } from "vite-plugin-shopify-theme-islands/events";
onIslandLoad(({ tag, duration }) => {
if (duration > 3000) {
performance.mark(`island-slow:${tag}`);
}
});
duration measures only the chunk fetch time — time spent waiting on directives (e.g. client:visible) is not included.
Report errors to a monitoring service
import { onIslandError } from "vite-plugin-shopify-theme-islands/events";
onIslandError(({ tag, error, attempt }) => {
Sentry.captureException(error, { extra: { island: tag, attempt } });
});
onIslandError fires on each retry attempt, on custom directive failures, and when directiveTimeout expires. attempt tells you which attempt failed — 1 is the initial load, 2 is the first retry, etc.
Subtree control and teardown
import { disconnect, observe, scan, unobserve } from "vite-plugin-shopify-theme-islands/revive";
scan(container);
observe(container);
unobserve(container);
disconnect();
disconnect() stops the shared runtime entirely and removes the Shopify Theme Editor lifecycle listeners registered by default. unobserve(root) is the narrower tool: it pauses one subtree and cancels pending built-in waits, retries, and custom directive cleanup inside it. If the runtime has not initialized yet because the document is still loading, disconnect() also unregisters the pending DOMContentLoaded startup listener so init never runs later.
The startup walk itself is now lifecycle-owned. The runtime resolves the root lazily at init time, then the lifecycle coordinator performs the initial walk, begins observing subtree additions, and keeps child islands gated behind queued parents until the parent resolves.
Load/error events and debug-ready groups are dispatched through the runtime surface, but the user-facing lifecycle behavior remains the same: startup is lazy, activation is subtree-aware, and teardown prevents later observation.
Shopify Theme Editor lifecycle (on by default)
The shared runtime registers listeners for Theme Editor section and block events and maps them to subtree helpers on the resolved DOM root:
| Event | Action |
|---|
shopify:section:load | observe(root) |
shopify:section:unload | unobserve(root) |
shopify:section:reorder, shopify:section:select, shopify:section:deselect | scan(root) |
shopify:block:select, shopify:block:deselect | scan(root) |
Roots are resolved from event.detail (sectionId / blockId) and event.target via src/shopify-lifecycle.ts (resolveLifecycleRoot). This keeps islands aligned when sections are swapped or reordered in the editor.
Raw DOM events (when type augmentation is in scope)
import "vite-plugin-shopify-theme-islands";
document.addEventListener("islands:load", (e) => {
console.log(e.detail.tag, e.detail.duration, e.detail.attempt);
});
The DocumentEventMap augmentation is declared in contract.ts and re-exported via the main package entry. Your app-side tsconfig has to include that import for the event typing to be available.
Common Mistakes
HIGH Raw addEventListener without types — e.detail is untyped
Wrong:
document.addEventListener("islands:load", (e) => {
console.log(e.detail.tag);
});
Correct:
import { onIslandLoad } from "vite-plugin-shopify-theme-islands/events";
onIslandLoad(({ tag }) => {
console.log(tag);
});
onIslandLoad and onIslandError are typed unconditionally regardless of tsconfig setup. Use them instead of raw document.addEventListener unless the DocumentEventMap augmentation is confirmed to be in scope.
Source: src/events.ts
CRITICAL lifecycle helpers imported from wrong entry point
Wrong:
import { disconnect } from "vite-plugin-shopify-theme-islands/island";
Correct:
import { disconnect, scan, observe, unobserve } from "vite-plugin-shopify-theme-islands/revive";
Only the virtual module (/revive) exports the shared helper surface bound to the plugin-managed runtime instance.
Source: src/revive-module.ts — buildReviveModuleSource() emits export const { disconnect, scan, observe, unobserve } = runtime
MEDIUM onIslandError fires on every retry, not just final failure
Wrong:
onIslandError(({ tag }) => {
markIslandBroken(tag);
});
Correct:
onIslandError(({ tag, error, attempt }) => {
if (attempt === 1) {
reportFirstFailure(tag, error);
}
});
With retry: { retries: 3 }, a single island can fire islands:error up to 4 times before exhausting retries. Use attempt to distinguish the initial failure from retries.
Source: src/activation-session.ts — surface.dispatchError(...) inside the loader failure path before retry check
MEDIUM islands:error fires for custom directive failures too
Wrong assumption:
onIslandError(({ tag, error }) => {
reportChunkLoadFailure(tag);
});
islands:error fires when any custom directive throws, rejects, or times out, not only when the island module's import() fails. The error value may be a directive error rather than a network or chunk error.
Source: src/activation-session.ts — handleDirectiveError dispatches islands:error
LOW Removed elements waiting on client:visible / client:interaction do not emit islands:error
If an element is removed from the DOM before a cancellable built-in directive resolves, the lifecycle coordinator cancels that activation attempt and the runtime treats it as expected teardown. No islands:error event is dispatched.
Source: src/cancellable-watchers.ts — cancelDetached() / watch() registry, called from src/lifecycle.ts