---
name: lifecycle
description: >
  Island 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.
type: core
library: vite-plugin-shopify-theme-islands
library_version: "1.1.0"
sources:
  - Rees1993/vite-plugin-shopify-theme-islands:src/events.ts
  - Rees1993/vite-plugin-shopify-theme-islands:src/index.ts
  - Rees1993/vite-plugin-shopify-theme-islands:src/runtime.ts
---

## Setup

```ts
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

```ts
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

```ts
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

```ts
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

```ts
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)

```ts
// 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:

```ts
// 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:

```ts
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:

```ts
import { disconnect } from "vite-plugin-shopify-theme-islands/runtime";
import { disconnect } from "vite-plugin-shopify-theme-islands/island";
```

Correct:

```ts
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:

```ts
onIslandError(({ tag }) => {
  // Assuming this fires once when the island permanently fails
  markIslandBroken(tag);
});
```

Correct:

```ts
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:

```ts
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`
