# Day 6: Component Lifecycle, Dynamic Activation, Cleanup, Preservation, and Morphing

**Onboarding track:** Alpine.js + htmx, co-equal architecture  
**Verified baseline:** Alpine.js 3.15.12 and htmx 2.0.10 (stable)  
**Verification date:** 2026-07-19  
**Compatibility boundary:** htmx 4.0.0-beta5 is a prerelease line and is not the syntax baseline for these lessons.

## Purpose

Make DOM replacement deliberate. Learn when local state resets, survives, is exceptionally preserved, or is reconciled, and how to initialize and clean up components under repeated swaps.

## How to use this document

Budget a working day. Read the lessons in order, type the examples instead of copying them blindly, perform every mini-task, and finish the exit exercise without looking at the answer notes. The examples use a fictional server-rendered operations application, but the ownership rules are framework-neutral.

A successful day ends with three artifacts:

1. working markup or a small runnable page;
2. written ownership notes for each state value and DOM region;
3. answers to the comprehension check.

## Learning outcomes

You will be able to:

- choose Reset, Survive, Preserve, or Reconcile before a swap;
- keep stable Alpine shells outside htmx targets;
- intentionally replace disposable Alpine roots;
- process Alpine-created `hx-*` markup with `htmx.process()`;
- use Alpine `init()` and `destroy()` for external resources;
- use htmx cleanup events for non-Alpine widgets;
- reject preservation and morphing when a better boundary solves the problem.

## 1. The governing fact

htmx replaces DOM. Alpine state is attached to component roots in that DOM.

Therefore every target needs a lifecycle decision before implementation.

## 2. The four lifecycle choices

### Reset

Replace the Alpine root and create a new instance.

Use for:

- submitted form replaced by success;
- inline editor replaced by saved record;
- row or card whose temporary menu should close;
- local draft that should clear after completion.

```html
<article id="invoice-42" x-data="{ menuOpen: false }">
    ...
    <button
        hx-get="/invoices/42/edit"
        hx-target="#invoice-42"
        hx-swap="outerHTML"
    >
        Edit
    </button>
</article>
```

The returned editor replaces the article. Reset is correct.

### Survive

Keep the Alpine root stable and replace an inner htmx target.

Use for:

- modal shell;
- drawer;
- filter panel;
- command palette;
- live panel with local expansion state.

```html
<aside x-data="{ open: false }">
    <button type="button" @click="open = !open">Cart</button>
    <section x-cloak x-show="open">
        <div id="cart-contents"></div>
    </section>
</aside>
```

htmx targets `#cart-contents`, not the `<aside>`.

### Preserve

Use `hx-preserve` only when live element identity must survive and moving the target is impractical.

```html
<div id="player" hx-preserve>
    <video controls src="/media/briefing.mp4"></video>
</div>
```

Requirements and cautions:

- the preserved element needs a stable `id`;
- the response should contain a matching ID;
- some elements do not preserve focus, caret, iframe, or media behavior perfectly;
- preservation may relocate a live node;
- history and OOB interactions need testing;
- preservation is not a substitute for ownership design.

### Reconcile

Use a morphing extension only after normal replacement and better target boundaries fail a demonstrated requirement.

Morphing adds identity, attribute reconciliation, plugin, and debugging complexity. It can preserve nodes, but it can also preserve stale local assumptions. Define IDs and state ownership first.

## 3. Lifecycle decision table

| Requirement | First choice |
| --- | --- |
| Local state should clear | Reset |
| Local shell should remain open | Survive |
| One expensive/live element must retain identity | Preserve |
| Large server component must reconcile without replacement | Reconcile, after evidence |
| Third-party widget can be isolated | Stable JavaScript island |
| No local state exists | htmx-only replacement |

## 4. Alpine initialization

Registered components may define `init()`:

```html
<script>
document.addEventListener('alpine:init', () => {
    Alpine.data('clock', () => ({
        now: new Date(),
        timer: null,

        init() {
            this.timer = window.setInterval(() => {
                this.now = new Date()
            }, 1000)
        },

        destroy() {
            window.clearInterval(this.timer)
        }
    }))
})
</script>
```

Initialization must be repeatable. A server response may create the component many times.

Do not assume a node from a previous instance still exists.

## 5. Cleanup with `destroy()`

Clean up resources that outlive ordinary DOM properties:

- intervals and timeouts;
- window/document listeners registered manually;
- observers;
- media-query listeners;
- subscriptions;
- third-party widget instances;
- browser API handles.

Alpine automatically calls a component’s `destroy()` before cleaning up that Alpine component. Repeated htmx swaps should not leak external resources.

A listener example:

```html
<script>
document.addEventListener('alpine:init', () => {
    Alpine.data('viewportWatcher', () => ({
        handler: null,
        width: window.innerWidth,

        init() {
            this.handler = () => {
                this.width = window.innerWidth
            }
            window.addEventListener('resize', this.handler)
        },

        destroy() {
            window.removeEventListener('resize', this.handler)
        }
    }))
})
</script>
```

Prefer Alpine event modifiers such as `.window` where they naturally manage listener lifetime.

## 6. Content inserted by htmx

When htmx inserts markup containing `x-data` or other Alpine directives, Alpine 3 normally observes DOM additions and initializes the new markup as a fresh component.

Operational rule:

- write initialization so it can run repeatedly;
- do not rely on removed nodes;
- verify initialization in browser integration tests;
- if using a special morph/compatibility path, follow that integration’s documentation.

Do not call `Alpine.start()` again after every swap.

## 7. Content inserted by Alpine

When Alpine creates a subtree containing `hx-*` attributes, htmx did not necessarily see those nodes during its initial processing. Process the new region after Alpine inserts it.

```html
<div
    x-data="{ mounted: false }"
    x-init="$watch('mounted', (value) => {
        if (!value) return

        $nextTick(() => {
            htmx.process($refs.remoteRegion)
        })
    })"
>
    <button type="button" @click="mounted = true">
        Mount remote panel
    </button>

    <template x-if="mounted">
        <section x-ref="remoteRegion">
            <button
                hx-get="/panel"
                hx-target="next div"
            >
                Load panel
            </button>
            <div></div>
        </section>
    </template>
</div>
```

Process only the new region. Do not repeatedly process the whole document.

A cleaner extracted method:

```html
<script>
document.addEventListener('alpine:init', () => {
    Alpine.data('remoteMount', () => ({
        mounted: false,

        mount() {
            this.mounted = true
            this.$nextTick(() => htmx.process(this.$refs.remoteRegion))
        }
    }))
})
</script>
```

## 8. Third-party widgets

A rich editor, chart, canvas, map, or drag engine may be an isolated JavaScript island.

Rules:

1. keep the island root outside ordinary htmx targets;
2. do not duplicate its internal state in Alpine;
3. expose durable values through controls or an explicit API;
4. emit semantic events;
5. document teardown if replacement is unavoidable.

Use `htmx:beforeCleanupElement` for a non-Alpine widget that must tear down before removal:

```html
<script>
document.body.addEventListener('htmx:beforeCleanupElement', (event) => {
    const element = event.detail.elt
    const editor = element.__editorInstance
    editor?.destroy()
})
</script>
```

Scope handlers narrowly. Do not scan every removed node with expensive logic unless necessary.

## 9. `x-ignore` as an isolation boundary

```html
<div x-data="{ open: true }">
    <section x-ignore id="third-party-editor">
        <!-- widget manages this subtree -->
    </section>
</div>
```

`x-ignore` tells Alpine not to initialize the subtree. It does not protect the region from htmx replacement. The htmx boundary still needs an explicit policy.

## 10. Identity rules

Distinguish:

- server IDs used for routes and canonical resources;
- DOM IDs used for targets and OOB swaps;
- stable IDs required by `hx-preserve`;
- Alpine-local generated IDs for labels, descriptions, or tabs.

Do not use a freshly generated Alpine ID for a server response that must target the same element later.

## 11. Focus and lifecycle

Replacement can remove the focused element.

For each swap, choose:

- focus remains outside target;
- returned markup uses a deliberate focus marker;
- focus moves to a stable shell;
- focus returns to the triggering control after completion;
- no focus movement is needed.

Test:

- validation loop;
- inline edit save/cancel;
- target deletion;
- history restoration;
- modal content replacement;
- polling update while the user is reading.

Do not let polling repeatedly steal focus.

## 12. Morphing decision gate

Before adding a morph extension, answer yes to all:

1. Is the replacement problem reproducible and user-visible?
2. Is moving the target boundary impossible or worse?
3. Are stable IDs defined?
4. Is the state owner documented?
5. Are Alpine and third-party widget semantics tested under morph?
6. Is focus behavior tested?
7. Is failure behavior understandable?
8. Is the extension version pinned?
9. Does the team accept the migration and debugging cost?

If not, redesign the target first.

## 13. Lifecycle SOP

For every mixed component:

```text
Alpine root:
htmx target:
Swap:
Reset / Survive / Preserve / Reconcile:
Stable IDs:
Initialization:
External resources:
Cleanup:
Dynamic Alpine-created htmx markup:
Third-party island:
Focus after swap:
History restoration behavior:
```

## 14. Day lab: live customer drawer

Build a drawer with:

- stable Alpine open/closed state;
- htmx-loaded customer details;
- an inner edit form that may swap repeatedly;
- a disposable menu inside each server-rendered customer card;
- a small third-party date picker or simulated widget with teardown;
- an Alpine-created optional remote panel that requires `htmx.process()`.

Instrument the page with console messages for `init()` and `destroy()`. Reopen and replace components repeatedly. Confirm no duplicate timers or listeners remain.

## Mini-tasks

### Task A: classify lifecycle

Choose Reset, Survive, Preserve, or Reconcile for:

- playing audio during a nearby refresh;
- an open modal with server validation;
- a row action menu after the row is saved;
- a chart with a specialized renderer;
- a long form where only one summary region changes.

### Task B: fix repeated initialization

A component calls `Alpine.start()` after every htmx swap. Replace that with the correct lifecycle design.

### Task C: dynamic activation

Mount an `x-if` subtree containing an htmx button and process only the new subtree.

### Task D: leak test

Create and destroy a timer component ten times and prove only one timer exists per live component.

## Comprehension check

1. What does Reset mean?
2. What is the preferred way to make Alpine state survive?
3. What stable requirement does `hx-preserve` have?
4. Why is morphing not a default?
5. When is `htmx.process()` needed?
6. Why should it target only the new subtree?
7. What does Alpine `destroy()` clean up?
8. When is `htmx:beforeCleanupElement` useful?
9. Does `x-ignore` prevent htmx replacement?
10. Why must polling avoid moving focus?

## Cheat sheet

```text
RESET       replace Alpine root
SURVIVE     keep Alpine root, replace inner target
PRESERVE    stable-ID live element, exceptional
RECONCILE   morph after demonstrated need
HTMX -> ALPINE MARKUP   normally fresh Alpine init
ALPINE -> HTMX MARKUP   htmx.process(newRegion)
EXTERNAL RESOURCE       init + matching destroy
RICH WIDGET             isolated stable island
```

## Exit exercise

Design a remote document editor shell. The shell remains open; server validation replaces the inner form; a third-party rich editor is isolated; a preview can refresh; and successful save resets disposable local draft flags. Document every lifecycle edge before coding.

## Exit criteria

You pass when repeated swaps produce no leaked listeners, accidental state loss, duplicate initialization, or unexplained preservation.

## Official reference shelf

Use these as the authority when a project version or behavior is uncertain:

- Alpine.js documentation: https://alpinejs.dev/
- Alpine.js directives: https://alpinejs.dev/directives/
- Alpine.js `Alpine.data()` lifecycle and `destroy()`: https://alpinejs.dev/globals/alpine-data
- htmx 2.x documentation: https://htmx.org/docs/
- htmx 2.x reference: https://htmx.org/reference/
- htmx 2.x events: https://htmx.org/events/
- htmx 4 migration guide: https://four.htmx.org/migration-guide-htmx-4/

> Version note: These exercises use htmx 2.x event names such as `htmx:afterSwap` and `htmx:afterSettle`. htmx 4 changes event naming and several defaults. Do not mechanically port the examples without the migration guide.
