@adobe/data-lit 0.10.9 → 0.10.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -1
- package/dist/hooks/component/hooks-controller.d.ts +28 -0
- package/dist/hooks/component/hooks-controller.integration.test.d.ts +1 -0
- package/dist/hooks/component/hooks-controller.integration.test.js +74 -0
- package/dist/hooks/component/hooks-controller.integration.test.js.map +1 -0
- package/dist/hooks/component/hooks-controller.js +96 -0
- package/dist/hooks/component/hooks-controller.js.map +1 -0
- package/dist/hooks/component/hooks-controller.test.d.ts +1 -0
- package/dist/hooks/component/hooks-controller.test.js +85 -0
- package/dist/hooks/component/hooks-controller.test.js.map +1 -0
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/index.js +1 -0
- package/dist/hooks/index.js.map +1 -1
- package/dist/hooks/with-hooks.js +5 -0
- package/dist/hooks/with-hooks.js.map +1 -1
- package/package.json +3 -2
- package/src/hooks/component/hooks-controller.integration.test.ts +88 -0
- package/src/hooks/component/hooks-controller.test.ts +94 -0
- package/src/hooks/component/hooks-controller.ts +104 -0
- package/src/hooks/index.ts +1 -0
- package/src/hooks/with-hooks.ts +5 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// © 2026 Adobe. MIT License. See /LICENSE for details.
|
|
2
|
+
|
|
3
|
+
import type { ReactiveController, ReactiveControllerHost } from "lit";
|
|
4
|
+
import type { Component } from "./component.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Per-host flag marking that the hook-lifecycle controller is already
|
|
8
|
+
* installed, so repeated renders (and double-wrapped `render` methods) do
|
|
9
|
+
* not register duplicate controllers.
|
|
10
|
+
*/
|
|
11
|
+
const HOOKS_CONTROLLER = Symbol("data-lit.hooksController");
|
|
12
|
+
|
|
13
|
+
type ReactiveHost = Component & ReactiveControllerHost & { [HOOKS_CONTROLLER]?: boolean };
|
|
14
|
+
|
|
15
|
+
function isReactiveHost(host: Component): host is ReactiveHost {
|
|
16
|
+
return "addController" in host && typeof host.addController === "function";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Dispose every effect-style hook slot and reset the hook cursor so the next
|
|
21
|
+
* render re-initializes hooks from scratch. Only slots that expose a
|
|
22
|
+
* `dispose` function (effects, subscriptions) are torn down; value slots
|
|
23
|
+
* (state, memo, ref) are simply dropped.
|
|
24
|
+
*/
|
|
25
|
+
function disposeHooks(host: Component): void {
|
|
26
|
+
const { hooks } = host;
|
|
27
|
+
if (hooks) {
|
|
28
|
+
for (const hook of hooks) {
|
|
29
|
+
(hook as { dispose?: () => void } | undefined)?.dispose?.();
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
// Invariant: every live subscription (useEffect / useObservable / ...) exposes a
|
|
33
|
+
// `dispose` and is torn down in the loop above, so nothing should fire after this.
|
|
34
|
+
// A useState setter closure that still runs post-disconnect would write into the
|
|
35
|
+
// emptied array and requestUpdate a detached host. That signals an un-torn-down
|
|
36
|
+
// subscription (a bug at the subscription site), not something to guard here.
|
|
37
|
+
host.hooks = [];
|
|
38
|
+
host.hookIndex = 0;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Give a hook host a real disconnect edge.
|
|
43
|
+
*
|
|
44
|
+
* The hook model stores every effect's cleanup in `host.hooks[i].dispose`,
|
|
45
|
+
* but {@link useEffect} only invokes it when dependencies change — never when
|
|
46
|
+
* the element leaves the DOM. Left alone, every `useObservable` /
|
|
47
|
+
* `useObservableValues` / `useEffect` subscription leaks on unmount and keeps
|
|
48
|
+
* firing `requestUpdate` on a detached element. `useConnected` is likewise
|
|
49
|
+
* inert because nothing dispatches the `"connected"` / `"disconnected"`
|
|
50
|
+
* events it listens for.
|
|
51
|
+
*
|
|
52
|
+
* This installs a single Lit {@link ReactiveController} — the one hook-friendly
|
|
53
|
+
* path to a genuine `hostDisconnected` — that:
|
|
54
|
+
* - on connect, dispatches `"connected"`, and on every *re*-connect forces a
|
|
55
|
+
* re-render so the (previously torn-down) hooks re-initialize and
|
|
56
|
+
* re-subscribe;
|
|
57
|
+
* - on disconnect, dispatches `"disconnected"` then disposes and clears all
|
|
58
|
+
* hook slots (full unmount semantics).
|
|
59
|
+
*
|
|
60
|
+
* It is installed lazily from inside the wrapped `render` (see {@link withHooks}),
|
|
61
|
+
* which runs after `connectedCallback`, so `addController` fires `hostConnected`
|
|
62
|
+
* synchronously on the first mount. The install is idempotent per host.
|
|
63
|
+
*
|
|
64
|
+
* Non-Lit hosts (no `addController`) are skipped; they may dispatch the
|
|
65
|
+
* lifecycle events themselves.
|
|
66
|
+
*/
|
|
67
|
+
export function installHooksController(host: Component): void {
|
|
68
|
+
if (!isReactiveHost(host) || host[HOOKS_CONTROLLER]) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
// Mark installed BEFORE addController below: on an already-connected host
|
|
72
|
+
// addController fires hostConnected synchronously, and setting the flag first
|
|
73
|
+
// guards against a re-entrant render re-installing a duplicate controller.
|
|
74
|
+
host[HOOKS_CONTROLLER] = true;
|
|
75
|
+
|
|
76
|
+
let connectedOnce = false;
|
|
77
|
+
const controller: ReactiveController = {
|
|
78
|
+
hostConnected() {
|
|
79
|
+
// The "connected" / "disconnected" events are for EXTERNAL listeners only.
|
|
80
|
+
// The internal useConnected does NOT depend on them: this fires before the
|
|
81
|
+
// render body attaches any listener, so useConnected is driven by its direct
|
|
82
|
+
// isConnected check plus useEffect cleanup instead.
|
|
83
|
+
host.dispatchEvent(new Event("connected"));
|
|
84
|
+
if (connectedOnce) {
|
|
85
|
+
// Reconnect: the previous disconnect disposed and cleared every
|
|
86
|
+
// hook slot, so re-render to re-run hooks and re-subscribe.
|
|
87
|
+
host.requestUpdate();
|
|
88
|
+
}
|
|
89
|
+
connectedOnce = true;
|
|
90
|
+
},
|
|
91
|
+
hostDisconnected() {
|
|
92
|
+
host.dispatchEvent(new Event("disconnected"));
|
|
93
|
+
disposeHooks(host);
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
try {
|
|
97
|
+
host.addController(controller);
|
|
98
|
+
} catch (error) {
|
|
99
|
+
// addController failed, so no controller is registered. Clear the flag so a
|
|
100
|
+
// later render can retry the install instead of being permanently skipped.
|
|
101
|
+
host[HOOKS_CONTROLLER] = undefined;
|
|
102
|
+
throw error;
|
|
103
|
+
}
|
|
104
|
+
}
|
package/src/hooks/index.ts
CHANGED
package/src/hooks/with-hooks.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import type { Component } from "./component/component.js";
|
|
4
4
|
import { Component_stack } from "./component/stack.js";
|
|
5
|
+
import { installHooksController } from "./component/hooks-controller.js";
|
|
5
6
|
|
|
6
7
|
export function withHooks<This extends Component, Args extends any[], Return>(
|
|
7
8
|
target: object,
|
|
@@ -10,6 +11,10 @@ export function withHooks<This extends Component, Args extends any[], Return>(
|
|
|
10
11
|
): TypedPropertyDescriptor<(this: This, ...args: Args) => Return> {
|
|
11
12
|
const originalMethod = descriptor.value!;
|
|
12
13
|
descriptor.value = function (this: This, ...args: Args): Return {
|
|
14
|
+
// Give the host a real disconnect edge so effect/subscription cleanups
|
|
15
|
+
// actually run on unmount. Idempotent per host, so double-wrapped
|
|
16
|
+
// `render` methods install it only once.
|
|
17
|
+
installHooksController(this);
|
|
13
18
|
Component_stack.push(this);
|
|
14
19
|
try {
|
|
15
20
|
return originalMethod.apply(this, args);
|