@adobe/data-lit 0.10.30 → 0.10.31
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 +12 -0
- package/dist/hooks/use-resize-observer.js +5 -2
- package/dist/hooks/use-resize-observer.js.map +1 -1
- package/dist/hooks/use-resize-observer.test.d.ts +1 -0
- package/dist/hooks/use-resize-observer.test.js +102 -0
- package/dist/hooks/use-resize-observer.test.js.map +1 -0
- package/dist/hooks/use-state.js +9 -7
- package/dist/hooks/use-state.js.map +1 -1
- package/dist/hooks/use-state.test.d.ts +1 -0
- package/dist/hooks/use-state.test.js +83 -0
- package/dist/hooks/use-state.test.js.map +1 -0
- package/package.json +2 -2
- package/src/hooks/use-resize-observer.test.ts +114 -0
- package/src/hooks/use-resize-observer.ts +5 -2
- package/src/hooks/use-state.test.ts +108 -0
- package/src/hooks/use-state.ts +14 -9
- package/tsconfig.tsbuildinfo +1 -1
package/README.md
CHANGED
|
@@ -16,6 +16,18 @@ import { ApplicationHost, DatabaseElement } from "@adobe/data-lit";
|
|
|
16
16
|
import { createDatabase } from "@adobe/data/ecs";
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
+
## State and resize callbacks
|
|
20
|
+
|
|
21
|
+
`useState` returns a setter whose identity is stable for the lifetime of the hook state. The setter
|
|
22
|
+
requests a render only when `Object.is` reports that the value has changed. Lazy initializers run once
|
|
23
|
+
per hook state, including when the initial value is `null` or `undefined`.
|
|
24
|
+
|
|
25
|
+
Treat state objects and arrays as immutable: pass a new object or array when its contents change.
|
|
26
|
+
Mutating a value in place and passing the same reference to the setter does not request a render.
|
|
27
|
+
|
|
28
|
+
`useResizeObserver` keeps its observer attached across renders and invokes the latest callback.
|
|
29
|
+
Inline callbacks do not cause the observer to disconnect and reattach.
|
|
30
|
+
|
|
19
31
|
## Hooks and the disconnect lifecycle
|
|
20
32
|
|
|
21
33
|
Hooks (`useState`, `useEffect`, `useObservable`, `useObservableValues`, `useMemo`, `useRef`, ...) follow
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// © 2026 Adobe. MIT License. See /LICENSE for details.
|
|
2
2
|
import { useEffect } from "./use-effect.js";
|
|
3
3
|
import { useElement } from "./use-element.js";
|
|
4
|
+
import { useRef } from "./use-ref.js";
|
|
4
5
|
/**
|
|
5
6
|
* Hook to observe element resizes. Calls callback with width, height, and the original entry
|
|
6
7
|
* when the element's size changes.
|
|
@@ -9,15 +10,17 @@ import { useElement } from "./use-element.js";
|
|
|
9
10
|
*/
|
|
10
11
|
export function useResizeObserver(onResize) {
|
|
11
12
|
const element = useElement();
|
|
13
|
+
const latestCallback = useRef(onResize);
|
|
14
|
+
latestCallback.current = onResize;
|
|
12
15
|
useEffect(() => {
|
|
13
16
|
const observer = new ResizeObserver(entries => {
|
|
14
17
|
for (const entry of entries) {
|
|
15
18
|
const { width, height } = entry.contentRect;
|
|
16
|
-
|
|
19
|
+
latestCallback.current({ width, height, entry });
|
|
17
20
|
}
|
|
18
21
|
});
|
|
19
22
|
observer.observe(element);
|
|
20
23
|
return () => observer.disconnect();
|
|
21
|
-
}, [element
|
|
24
|
+
}, [element]);
|
|
22
25
|
}
|
|
23
26
|
//# sourceMappingURL=use-resize-observer.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-resize-observer.js","sourceRoot":"","sources":["../../src/hooks/use-resize-observer.ts"],"names":[],"mappings":"AAAA,uDAAuD;AAEvD,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAC5C,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"use-resize-observer.js","sourceRoot":"","sources":["../../src/hooks/use-resize-observer.ts"],"names":[],"mappings":"AAAA,uDAAuD;AAEvD,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAC5C,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAQtC;;;;;GAKG;AACH,MAAM,UAAU,iBAAiB,CAAC,QAAoC;IAClE,MAAM,OAAO,GAAG,UAAU,EAAE,CAAC;IAC7B,MAAM,cAAc,GAAG,MAAM,CAAC,QAAQ,CAAC,CAAC;IACxC,cAAc,CAAC,OAAO,GAAG,QAAQ,CAAC;IAElC,SAAS,CAAC,GAAG,EAAE;QACX,MAAM,QAAQ,GAAG,IAAI,cAAc,CAAC,OAAO,CAAC,EAAE;YAC1C,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;gBAC1B,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,KAAK,CAAC,WAAW,CAAC;gBAC5C,cAAc,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,CAAC;YACrD,CAAC;QACL,CAAC,CAAC,CAAC;QACH,QAAQ,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;QAC1B,OAAO,GAAG,EAAE,CAAC,QAAQ,CAAC,UAAU,EAAE,CAAC;IACvC,CAAC,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC;AAClB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
import { afterEach, describe, it, expect, vi } from "vitest";
|
|
3
|
+
import { LitElement, html } from "lit";
|
|
4
|
+
import { attachDecorator } from "./attach-decorator.js";
|
|
5
|
+
import { withHooks } from "./with-hooks.js";
|
|
6
|
+
import { useMemo } from "./use-memo.js";
|
|
7
|
+
import { useResizeObserver } from "./use-resize-observer.js";
|
|
8
|
+
import { useState } from "./use-state.js";
|
|
9
|
+
class ResizeProbeElement extends LitElement {
|
|
10
|
+
constructor() {
|
|
11
|
+
super();
|
|
12
|
+
this.renders = 0;
|
|
13
|
+
this.offset = 0;
|
|
14
|
+
this.widths = [];
|
|
15
|
+
this.calculateCallback = vi.fn((setWidth) => setWidth);
|
|
16
|
+
attachDecorator(this, "render", withHooks);
|
|
17
|
+
}
|
|
18
|
+
render() {
|
|
19
|
+
this.renders++;
|
|
20
|
+
const [width, setWidth] = useState(0);
|
|
21
|
+
const memoizedSetter = useMemo(() => this.calculateCallback(setWidth), [setWidth]);
|
|
22
|
+
const offset = this.offset;
|
|
23
|
+
useResizeObserver(info => {
|
|
24
|
+
this.widths.push(info.width + offset);
|
|
25
|
+
memoizedSetter(Math.floor(info.width + offset));
|
|
26
|
+
});
|
|
27
|
+
return html `<span>${width}</span>`;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
customElements.define("resize-state-probe", ResizeProbeElement);
|
|
31
|
+
function mockResizeObserver() {
|
|
32
|
+
const observers = [];
|
|
33
|
+
vi.stubGlobal("ResizeObserver", class {
|
|
34
|
+
constructor(callback) {
|
|
35
|
+
this.observe = vi.fn();
|
|
36
|
+
this.disconnect = vi.fn();
|
|
37
|
+
observers.push({
|
|
38
|
+
emit: width => callback([
|
|
39
|
+
{ contentRect: { width, height: 20 } },
|
|
40
|
+
], this),
|
|
41
|
+
observe: this.observe,
|
|
42
|
+
disconnect: this.disconnect,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
return observers;
|
|
47
|
+
}
|
|
48
|
+
afterEach(async () => {
|
|
49
|
+
document.body.replaceChildren();
|
|
50
|
+
await new Promise(resolve => queueMicrotask(resolve));
|
|
51
|
+
vi.unstubAllGlobals();
|
|
52
|
+
});
|
|
53
|
+
describe("useResizeObserver with state", () => {
|
|
54
|
+
it("keeps one observer, uses the latest inline callback, and skips unchanged widths", async () => {
|
|
55
|
+
const observers = mockResizeObserver();
|
|
56
|
+
const element = document.createElement("resize-state-probe");
|
|
57
|
+
document.body.appendChild(element);
|
|
58
|
+
await element.updateComplete;
|
|
59
|
+
const observer = observers[0];
|
|
60
|
+
expect(observer.observe).toHaveBeenCalledExactlyOnceWith(element);
|
|
61
|
+
observer.emit(100.2);
|
|
62
|
+
await element.updateComplete;
|
|
63
|
+
expect(element.shadowRoot?.textContent).toBe("100");
|
|
64
|
+
expect(element.renders).toBe(2);
|
|
65
|
+
expect(observers).toHaveLength(1);
|
|
66
|
+
expect(observer.disconnect).not.toHaveBeenCalled();
|
|
67
|
+
expect(element.calculateCallback).toHaveBeenCalledTimes(1);
|
|
68
|
+
const requestUpdate = vi.spyOn(element, "requestUpdate");
|
|
69
|
+
observer.emit(100.8);
|
|
70
|
+
await element.updateComplete;
|
|
71
|
+
expect(requestUpdate).not.toHaveBeenCalled();
|
|
72
|
+
expect(element.renders).toBe(2);
|
|
73
|
+
element.offset = 10;
|
|
74
|
+
element.requestUpdate();
|
|
75
|
+
await element.updateComplete;
|
|
76
|
+
observer.emit(200);
|
|
77
|
+
await element.updateComplete;
|
|
78
|
+
expect(element.widths).toEqual([100.2, 100.8, 210]);
|
|
79
|
+
expect(element.shadowRoot?.textContent).toBe("210");
|
|
80
|
+
expect(observers).toHaveLength(1);
|
|
81
|
+
expect(observer.disconnect).not.toHaveBeenCalled();
|
|
82
|
+
expect(element.calculateCallback).toHaveBeenCalledTimes(1);
|
|
83
|
+
});
|
|
84
|
+
it("disconnects on unmount and creates fresh state and observer on remount", async () => {
|
|
85
|
+
const observers = mockResizeObserver();
|
|
86
|
+
const element = document.createElement("resize-state-probe");
|
|
87
|
+
document.body.appendChild(element);
|
|
88
|
+
await element.updateComplete;
|
|
89
|
+
observers[0].emit(100);
|
|
90
|
+
await element.updateComplete;
|
|
91
|
+
element.remove();
|
|
92
|
+
await new Promise(resolve => queueMicrotask(resolve));
|
|
93
|
+
expect(observers[0].disconnect).toHaveBeenCalledTimes(1);
|
|
94
|
+
document.body.appendChild(element);
|
|
95
|
+
await element.updateComplete;
|
|
96
|
+
expect(element.shadowRoot?.textContent).toBe("0");
|
|
97
|
+
expect(observers).toHaveLength(2);
|
|
98
|
+
expect(observers[1].observe).toHaveBeenCalledExactlyOnceWith(element);
|
|
99
|
+
expect(element.calculateCallback).toHaveBeenCalledTimes(2);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
//# sourceMappingURL=use-resize-observer.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-resize-observer.test.js","sourceRoot":"","sources":["../../src/hooks/use-resize-observer.test.ts"],"names":[],"mappings":"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"}
|
package/dist/hooks/use-state.js
CHANGED
|
@@ -3,13 +3,15 @@ import { Component_stack } from "./component/stack.js";
|
|
|
3
3
|
export function useState(initial) {
|
|
4
4
|
const component = Component_stack.active();
|
|
5
5
|
const hookIndex = component.hookIndex++;
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
6
|
+
const state = component.hooks[hookIndex] ??= {
|
|
7
|
+
value: typeof initial === "function" ? initial() : initial,
|
|
8
|
+
setValue: (newValue) => {
|
|
9
|
+
if (Object.is(state.value, newValue))
|
|
10
|
+
return;
|
|
11
|
+
state.value = newValue;
|
|
11
12
|
component.requestUpdate();
|
|
12
|
-
}
|
|
13
|
-
|
|
13
|
+
},
|
|
14
|
+
};
|
|
15
|
+
return [state.value, state.setValue];
|
|
14
16
|
}
|
|
15
17
|
//# sourceMappingURL=use-state.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-state.js","sourceRoot":"","sources":["../../src/hooks/use-state.ts"],"names":[],"mappings":"AAAA,uDAAuD;AAEvD,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"use-state.js","sourceRoot":"","sources":["../../src/hooks/use-state.ts"],"names":[],"mappings":"AAAA,uDAAuD;AAEvD,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AASvD,MAAM,UAAU,QAAQ,CAAI,OAAsB;IAC9C,MAAM,SAAS,GAAG,eAAe,CAAC,MAAM,EAAE,CAAC;IAC3C,MAAM,SAAS,GAAG,SAAS,CAAC,SAAS,EAAE,CAAC;IACxC,MAAM,KAAK,GAAa,SAAS,CAAC,KAAK,CAAC,SAAS,CAAC,KAAK;QACnD,KAAK,EAAE,OAAO,OAAO,KAAK,UAAU,CAAC,CAAC,CAAE,OAAmB,EAAE,CAAC,CAAC,CAAC,OAAO;QACvE,QAAQ,EAAE,CAAC,QAAW,EAAE,EAAE;YACtB,IAAI,MAAM,CAAC,EAAE,CAAC,KAAK,CAAC,KAAK,EAAE,QAAQ,CAAC;gBAAE,OAAO;YAC7C,KAAK,CAAC,KAAK,GAAG,QAAQ,CAAC;YACvB,SAAS,CAAC,aAAa,EAAE,CAAC;QAC9B,CAAC;KACJ,CAAC;IACF,OAAO,CAAC,KAAK,CAAC,KAAK,EAAE,KAAK,CAAC,QAAQ,CAAC,CAAC;AACzC,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import { Component_stack } from "./component/stack.js";
|
|
3
|
+
import { useState } from "./use-state.js";
|
|
4
|
+
function createHost() {
|
|
5
|
+
return Object.assign(new EventTarget(), {
|
|
6
|
+
isConnected: true,
|
|
7
|
+
hookIndex: 0,
|
|
8
|
+
hooks: [],
|
|
9
|
+
updatedListeners: new Set(),
|
|
10
|
+
requestUpdate: vi.fn(),
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
function render(host, callback) {
|
|
14
|
+
Component_stack.push(host);
|
|
15
|
+
try {
|
|
16
|
+
return callback();
|
|
17
|
+
}
|
|
18
|
+
finally {
|
|
19
|
+
Component_stack.pop();
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
describe("useState", () => {
|
|
23
|
+
it("keeps the setter stable across renders and updates", () => {
|
|
24
|
+
const host = createHost();
|
|
25
|
+
const [, setValue] = render(host, () => useState(0));
|
|
26
|
+
expect(render(host, () => useState(0))[1]).toBe(setValue);
|
|
27
|
+
setValue(1);
|
|
28
|
+
const [value, nextSetter] = render(host, () => useState(0));
|
|
29
|
+
expect(value).toBe(1);
|
|
30
|
+
expect(nextSetter).toBe(setValue);
|
|
31
|
+
expect(host.requestUpdate).toHaveBeenCalledTimes(1);
|
|
32
|
+
});
|
|
33
|
+
it.each([0, NaN, null, undefined, "value", false, {}, []])("does not request an update for an Object.is-equal value: %s", initial => {
|
|
34
|
+
const host = createHost();
|
|
35
|
+
const [, setValue] = render(host, () => useState(initial));
|
|
36
|
+
setValue(initial);
|
|
37
|
+
setValue(initial);
|
|
38
|
+
expect(host.requestUpdate).not.toHaveBeenCalled();
|
|
39
|
+
});
|
|
40
|
+
it("compares against the latest value before another render", () => {
|
|
41
|
+
const host = createHost();
|
|
42
|
+
const [, setValue] = render(host, () => useState(0));
|
|
43
|
+
setValue(1);
|
|
44
|
+
setValue(1);
|
|
45
|
+
setValue(2);
|
|
46
|
+
expect(host.requestUpdate).toHaveBeenCalledTimes(2);
|
|
47
|
+
expect(render(host, () => useState(0))[0]).toBe(2);
|
|
48
|
+
});
|
|
49
|
+
it("distinguishes positive and negative zero", () => {
|
|
50
|
+
const host = createHost();
|
|
51
|
+
const [, setValue] = render(host, () => useState(0));
|
|
52
|
+
setValue(-0);
|
|
53
|
+
expect(host.requestUpdate).toHaveBeenCalledTimes(1);
|
|
54
|
+
expect(render(host, () => useState(0))[0]).toBe(-0);
|
|
55
|
+
});
|
|
56
|
+
it.each([null, undefined])("initializes a nullish value only once: %s", initial => {
|
|
57
|
+
const host = createHost();
|
|
58
|
+
const initialize = vi.fn(() => initial);
|
|
59
|
+
render(host, () => useState(initialize));
|
|
60
|
+
render(host, () => useState(initialize));
|
|
61
|
+
expect(initialize).toHaveBeenCalledTimes(1);
|
|
62
|
+
});
|
|
63
|
+
it.each([null, undefined])("preserves a nullish update: %s", nextValue => {
|
|
64
|
+
const host = createHost();
|
|
65
|
+
const [, setValue] = render(host, () => useState(1));
|
|
66
|
+
setValue(nextValue);
|
|
67
|
+
expect(render(host, () => useState(1))[0]).toBe(nextValue);
|
|
68
|
+
});
|
|
69
|
+
it("keeps state slots and hosts independent", () => {
|
|
70
|
+
const host = createHost();
|
|
71
|
+
const otherHost = createHost();
|
|
72
|
+
const read = () => [useState(0), useState(10)];
|
|
73
|
+
const [first, second] = render(host, read);
|
|
74
|
+
const [other] = render(otherHost, read);
|
|
75
|
+
first[1](1);
|
|
76
|
+
expect(render(host, read).map(([value]) => value)).toEqual([1, 10]);
|
|
77
|
+
expect(render(otherHost, read).map(([value]) => value)).toEqual([0, 10]);
|
|
78
|
+
expect(first[1]).not.toBe(second[1]);
|
|
79
|
+
expect(first[1]).not.toBe(other[1]);
|
|
80
|
+
expect(otherHost.requestUpdate).not.toHaveBeenCalled();
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
//# sourceMappingURL=use-state.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-state.test.js","sourceRoot":"","sources":["../../src/hooks/use-state.test.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adobe/data-lit",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.31",
|
|
4
4
|
"description": "Adobe data Lit bindings - hooks, elements, decorators",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"private": false,
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"access": "public"
|
|
13
13
|
},
|
|
14
14
|
"dependencies": {
|
|
15
|
-
"@adobe/data": "0.10.
|
|
15
|
+
"@adobe/data": "0.10.31"
|
|
16
16
|
},
|
|
17
17
|
"peerDependencies": {
|
|
18
18
|
"lit": "^3.3.1"
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
import { afterEach, describe, it, expect, vi } from "vitest";
|
|
3
|
+
import { LitElement, html } from "lit";
|
|
4
|
+
import { attachDecorator } from "./attach-decorator.js";
|
|
5
|
+
import { withHooks } from "./with-hooks.js";
|
|
6
|
+
import { useMemo } from "./use-memo.js";
|
|
7
|
+
import { useResizeObserver } from "./use-resize-observer.js";
|
|
8
|
+
import { useState } from "./use-state.js";
|
|
9
|
+
|
|
10
|
+
class ResizeProbeElement extends LitElement {
|
|
11
|
+
renders = 0;
|
|
12
|
+
offset = 0;
|
|
13
|
+
widths: number[] = [];
|
|
14
|
+
calculateCallback = vi.fn((setWidth: (value: number) => void) => setWidth);
|
|
15
|
+
|
|
16
|
+
constructor() {
|
|
17
|
+
super();
|
|
18
|
+
attachDecorator(this, "render", withHooks);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
render() {
|
|
22
|
+
this.renders++;
|
|
23
|
+
const [width, setWidth] = useState(0);
|
|
24
|
+
const memoizedSetter = useMemo(() => this.calculateCallback(setWidth), [setWidth]);
|
|
25
|
+
const offset = this.offset;
|
|
26
|
+
useResizeObserver(info => {
|
|
27
|
+
this.widths.push(info.width + offset);
|
|
28
|
+
memoizedSetter(Math.floor(info.width + offset));
|
|
29
|
+
});
|
|
30
|
+
return html`<span>${width}</span>`;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
customElements.define("resize-state-probe", ResizeProbeElement);
|
|
34
|
+
|
|
35
|
+
function mockResizeObserver() {
|
|
36
|
+
const observers: { emit: (width: number) => void; observe: ReturnType<typeof vi.fn>; disconnect: ReturnType<typeof vi.fn> }[] = [];
|
|
37
|
+
vi.stubGlobal("ResizeObserver", class {
|
|
38
|
+
observe = vi.fn();
|
|
39
|
+
disconnect = vi.fn();
|
|
40
|
+
|
|
41
|
+
constructor(callback: ResizeObserverCallback) {
|
|
42
|
+
observers.push({
|
|
43
|
+
emit: width => callback([
|
|
44
|
+
{ contentRect: { width, height: 20 } } as ResizeObserverEntry,
|
|
45
|
+
], this as unknown as ResizeObserver),
|
|
46
|
+
observe: this.observe,
|
|
47
|
+
disconnect: this.disconnect,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
return observers;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
afterEach(async () => {
|
|
55
|
+
document.body.replaceChildren();
|
|
56
|
+
await new Promise<void>(resolve => queueMicrotask(resolve));
|
|
57
|
+
vi.unstubAllGlobals();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe("useResizeObserver with state", () => {
|
|
61
|
+
it("keeps one observer, uses the latest inline callback, and skips unchanged widths", async () => {
|
|
62
|
+
const observers = mockResizeObserver();
|
|
63
|
+
const element = document.createElement("resize-state-probe") as ResizeProbeElement;
|
|
64
|
+
document.body.appendChild(element);
|
|
65
|
+
await element.updateComplete;
|
|
66
|
+
const observer = observers[0];
|
|
67
|
+
expect(observer.observe).toHaveBeenCalledExactlyOnceWith(element);
|
|
68
|
+
|
|
69
|
+
observer.emit(100.2);
|
|
70
|
+
await element.updateComplete;
|
|
71
|
+
expect(element.shadowRoot?.textContent).toBe("100");
|
|
72
|
+
expect(element.renders).toBe(2);
|
|
73
|
+
expect(observers).toHaveLength(1);
|
|
74
|
+
expect(observer.disconnect).not.toHaveBeenCalled();
|
|
75
|
+
expect(element.calculateCallback).toHaveBeenCalledTimes(1);
|
|
76
|
+
|
|
77
|
+
const requestUpdate = vi.spyOn(element, "requestUpdate");
|
|
78
|
+
observer.emit(100.8);
|
|
79
|
+
await element.updateComplete;
|
|
80
|
+
expect(requestUpdate).not.toHaveBeenCalled();
|
|
81
|
+
expect(element.renders).toBe(2);
|
|
82
|
+
|
|
83
|
+
element.offset = 10;
|
|
84
|
+
element.requestUpdate();
|
|
85
|
+
await element.updateComplete;
|
|
86
|
+
observer.emit(200);
|
|
87
|
+
await element.updateComplete;
|
|
88
|
+
expect(element.widths).toEqual([100.2, 100.8, 210]);
|
|
89
|
+
expect(element.shadowRoot?.textContent).toBe("210");
|
|
90
|
+
expect(observers).toHaveLength(1);
|
|
91
|
+
expect(observer.disconnect).not.toHaveBeenCalled();
|
|
92
|
+
expect(element.calculateCallback).toHaveBeenCalledTimes(1);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("disconnects on unmount and creates fresh state and observer on remount", async () => {
|
|
96
|
+
const observers = mockResizeObserver();
|
|
97
|
+
const element = document.createElement("resize-state-probe") as ResizeProbeElement;
|
|
98
|
+
document.body.appendChild(element);
|
|
99
|
+
await element.updateComplete;
|
|
100
|
+
observers[0].emit(100);
|
|
101
|
+
await element.updateComplete;
|
|
102
|
+
|
|
103
|
+
element.remove();
|
|
104
|
+
await new Promise<void>(resolve => queueMicrotask(resolve));
|
|
105
|
+
expect(observers[0].disconnect).toHaveBeenCalledTimes(1);
|
|
106
|
+
|
|
107
|
+
document.body.appendChild(element);
|
|
108
|
+
await element.updateComplete;
|
|
109
|
+
expect(element.shadowRoot?.textContent).toBe("0");
|
|
110
|
+
expect(observers).toHaveLength(2);
|
|
111
|
+
expect(observers[1].observe).toHaveBeenCalledExactlyOnceWith(element);
|
|
112
|
+
expect(element.calculateCallback).toHaveBeenCalledTimes(2);
|
|
113
|
+
});
|
|
114
|
+
});
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { useEffect } from "./use-effect.js";
|
|
4
4
|
import { useElement } from "./use-element.js";
|
|
5
|
+
import { useRef } from "./use-ref.js";
|
|
5
6
|
|
|
6
7
|
export interface ResizeInfo {
|
|
7
8
|
width: number;
|
|
@@ -17,15 +18,17 @@ export interface ResizeInfo {
|
|
|
17
18
|
*/
|
|
18
19
|
export function useResizeObserver(onResize: (info: ResizeInfo) => void) {
|
|
19
20
|
const element = useElement();
|
|
21
|
+
const latestCallback = useRef(onResize);
|
|
22
|
+
latestCallback.current = onResize;
|
|
20
23
|
|
|
21
24
|
useEffect(() => {
|
|
22
25
|
const observer = new ResizeObserver(entries => {
|
|
23
26
|
for (const entry of entries) {
|
|
24
27
|
const { width, height } = entry.contentRect;
|
|
25
|
-
|
|
28
|
+
latestCallback.current({ width, height, entry });
|
|
26
29
|
}
|
|
27
30
|
});
|
|
28
31
|
observer.observe(element);
|
|
29
32
|
return () => observer.disconnect();
|
|
30
|
-
}, [element
|
|
33
|
+
}, [element]);
|
|
31
34
|
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import type { Component } from "./component/component.js";
|
|
3
|
+
import { Component_stack } from "./component/stack.js";
|
|
4
|
+
import { useState } from "./use-state.js";
|
|
5
|
+
|
|
6
|
+
function createHost(): Component {
|
|
7
|
+
return Object.assign(new EventTarget(), {
|
|
8
|
+
isConnected: true,
|
|
9
|
+
hookIndex: 0,
|
|
10
|
+
hooks: [],
|
|
11
|
+
updatedListeners: new Set<() => void>(),
|
|
12
|
+
requestUpdate: vi.fn(),
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function render<T>(host: Component, callback: () => T): T {
|
|
17
|
+
Component_stack.push(host);
|
|
18
|
+
try {
|
|
19
|
+
return callback();
|
|
20
|
+
} finally {
|
|
21
|
+
Component_stack.pop();
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
describe("useState", () => {
|
|
26
|
+
it("keeps the setter stable across renders and updates", () => {
|
|
27
|
+
const host = createHost();
|
|
28
|
+
const [, setValue] = render(host, () => useState(0));
|
|
29
|
+
expect(render(host, () => useState(0))[1]).toBe(setValue);
|
|
30
|
+
|
|
31
|
+
setValue(1);
|
|
32
|
+
|
|
33
|
+
const [value, nextSetter] = render(host, () => useState(0));
|
|
34
|
+
expect(value).toBe(1);
|
|
35
|
+
expect(nextSetter).toBe(setValue);
|
|
36
|
+
expect(host.requestUpdate).toHaveBeenCalledTimes(1);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it.each([0, NaN, null, undefined, "value", false, {}, []])(
|
|
40
|
+
"does not request an update for an Object.is-equal value: %s",
|
|
41
|
+
initial => {
|
|
42
|
+
const host = createHost();
|
|
43
|
+
const [, setValue] = render(host, () => useState(initial));
|
|
44
|
+
|
|
45
|
+
setValue(initial);
|
|
46
|
+
setValue(initial);
|
|
47
|
+
|
|
48
|
+
expect(host.requestUpdate).not.toHaveBeenCalled();
|
|
49
|
+
},
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
it("compares against the latest value before another render", () => {
|
|
53
|
+
const host = createHost();
|
|
54
|
+
const [, setValue] = render(host, () => useState(0));
|
|
55
|
+
|
|
56
|
+
setValue(1);
|
|
57
|
+
setValue(1);
|
|
58
|
+
setValue(2);
|
|
59
|
+
|
|
60
|
+
expect(host.requestUpdate).toHaveBeenCalledTimes(2);
|
|
61
|
+
expect(render(host, () => useState(0))[0]).toBe(2);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("distinguishes positive and negative zero", () => {
|
|
65
|
+
const host = createHost();
|
|
66
|
+
const [, setValue] = render(host, () => useState(0));
|
|
67
|
+
|
|
68
|
+
setValue(-0);
|
|
69
|
+
|
|
70
|
+
expect(host.requestUpdate).toHaveBeenCalledTimes(1);
|
|
71
|
+
expect(render(host, () => useState(0))[0]).toBe(-0);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it.each([null, undefined])("initializes a nullish value only once: %s", initial => {
|
|
75
|
+
const host = createHost();
|
|
76
|
+
const initialize = vi.fn(() => initial);
|
|
77
|
+
|
|
78
|
+
render(host, () => useState(initialize));
|
|
79
|
+
render(host, () => useState(initialize));
|
|
80
|
+
|
|
81
|
+
expect(initialize).toHaveBeenCalledTimes(1);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it.each([null, undefined])("preserves a nullish update: %s", nextValue => {
|
|
85
|
+
const host = createHost();
|
|
86
|
+
const [, setValue] = render(host, () => useState<number | null | undefined>(1));
|
|
87
|
+
|
|
88
|
+
setValue(nextValue);
|
|
89
|
+
|
|
90
|
+
expect(render(host, () => useState(1))[0]).toBe(nextValue);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("keeps state slots and hosts independent", () => {
|
|
94
|
+
const host = createHost();
|
|
95
|
+
const otherHost = createHost();
|
|
96
|
+
const read = () => [useState(0), useState(10)] as const;
|
|
97
|
+
const [first, second] = render(host, read);
|
|
98
|
+
const [other] = render(otherHost, read);
|
|
99
|
+
|
|
100
|
+
first[1](1);
|
|
101
|
+
|
|
102
|
+
expect(render(host, read).map(([value]) => value)).toEqual([1, 10]);
|
|
103
|
+
expect(render(otherHost, read).map(([value]) => value)).toEqual([0, 10]);
|
|
104
|
+
expect(first[1]).not.toBe(second[1]);
|
|
105
|
+
expect(first[1]).not.toBe(other[1]);
|
|
106
|
+
expect(otherHost.requestUpdate).not.toHaveBeenCalled();
|
|
107
|
+
});
|
|
108
|
+
});
|
package/src/hooks/use-state.ts
CHANGED
|
@@ -2,18 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
import { Component_stack } from "./component/stack.js";
|
|
4
4
|
|
|
5
|
+
interface State<T> {
|
|
6
|
+
value: T;
|
|
7
|
+
setValue: (value: T) => void;
|
|
8
|
+
}
|
|
9
|
+
|
|
5
10
|
export function useState<T>(initializer: () => T): [T, (value: T) => void]
|
|
6
11
|
export function useState<T>(initialValue: T): [T, (value: T) => void]
|
|
7
|
-
export function useState<T>(initial: () => T) {
|
|
12
|
+
export function useState<T>(initial: T | (() => T)): [T, (value: T) => void] {
|
|
8
13
|
const component = Component_stack.active();
|
|
9
14
|
const hookIndex = component.hookIndex++;
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
+
const state: State<T> = component.hooks[hookIndex] ??= {
|
|
16
|
+
value: typeof initial === "function" ? (initial as () => T)() : initial,
|
|
17
|
+
setValue: (newValue: T) => {
|
|
18
|
+
if (Object.is(state.value, newValue)) return;
|
|
19
|
+
state.value = newValue;
|
|
15
20
|
component.requestUpdate();
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
return [state.value, state.setValue];
|
|
19
24
|
}
|