lifecycle
coreIsland lifecycle events and SPA 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. disconnect() from the virtual module revive for SPA navigation teardown.
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);
});
// Remove listeners when no longer needed
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 and on custom directive failures. attempt tells you which attempt failed — 1 is the initial load, 2 is the first retry, etc.
Teardown for SPA navigation
import { disconnect } from "vite-plugin-shopify-theme-islands/revive";
// Before navigating away / unmounting the page
disconnect();
disconnect() stops the MutationObserver and prevents new islands from activating. Call it before SPA page transitions to avoid activating islands from the previous page's DOM.
Raw DOM events (when type augmentation is in scope)
// DocumentEventMap augmentation is exported from the main package
import type {} from "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 the main package's index.ts. It is only in scope when the import is present in the same tsconfig compilation.
Common Mistakes
HIGH Raw addEventListener without types — e.detail is untyped
Wrong:
// No import from the package — e is Event, detail is unknown
document.addEventListener("islands:load", (e) => {
console.log(e.detail.tag); // TypeScript error or any
});
Correct:
import { onIslandLoad } from "vite-plugin-shopify-theme-islands/events";
onIslandLoad(({ tag }) => {
console.log(tag); // string, always typed
});
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 disconnect imported from wrong entry point
Wrong:
import { disconnect } from "vite-plugin-shopify-theme-islands/runtime";
import { disconnect } from "vite-plugin-shopify-theme-islands/island";
Correct:
import { disconnect } from "vite-plugin-shopify-theme-islands/revive";
Only the virtual module (/revive) exports the disconnect bound to the plugin-managed revive() instance. Importing from other entry points references a different or nonexistent instance.
Source: src/index.ts — virtual module export const { disconnect } = _islands(...)
MEDIUM onIslandError fires on every retry, not just final failure
Wrong:
onIslandError(({ tag }) => {
// Assuming this fires once when the island permanently fails
markIslandBroken(tag);
});
Correct:
onIslandError(({ tag, error, attempt }) => {
// attempt === 1 is the first failure; higher values are retries
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/runtime.ts — dispatch("islands:error", ...) inside .catch() before retry check
MEDIUM islands:error fires for custom directive failures too
Wrong assumption:
onIslandError(({ tag, error }) => {
// Assuming this only fires for failed dynamic import()
reportChunkLoadFailure(tag);
});
islands:error fires when any custom directive throws or rejects, 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/runtime.ts — handleDirectiveError dispatches islands:error