@adobe-commerce/elsie 2.1.0-beta.3 → 2.1.0-beta.5
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/CHANGELOG.md +48 -0
- package/package.json +1 -1
- package/src/lib/slot.tsx +56 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,53 @@
|
|
|
1
1
|
# @adobe-commerce/elsie
|
|
2
2
|
|
|
3
|
+
## 2.1.0-beta.5
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 4cc7219: Fix a memory leak in `Slot`'s VNode cache (added to fix repeated
|
|
8
|
+
`insertBefore` crashes from replay churn): entries were never pruned, so a
|
|
9
|
+
`slot` callback that keeps calling `replaceWith`/`appendChild`/`prependChild`
|
|
10
|
+
with newly constructed elements over time (rather than mutating one element in
|
|
11
|
+
place) would pin every historical element in memory for the Slot's whole
|
|
12
|
+
lifetime. Entries are now evicted via the grafted element's `ref` callback
|
|
13
|
+
firing with `null`, which Preact does precisely when that VNode is detached
|
|
14
|
+
(superseded by a different element, or the Slot unmounting).
|
|
15
|
+
|
|
16
|
+
See `docs/slot-dom-graft-race.md` for the full trace, including a
|
|
17
|
+
pre-existing, unrelated gap this surfaced: calling `replaceWith` a second time
|
|
18
|
+
with a different element doesn't remove the first one.
|
|
19
|
+
|
|
20
|
+
- ba3bbe7: Fix a recurring `insertBefore` crash in `Slot` when content grafted
|
|
21
|
+
via `replaceWith`/`appendChild`/`prependChild` is re-applied on unrelated
|
|
22
|
+
re-renders. `Slot` replays its registered methods on every render pass (by
|
|
23
|
+
design, so `onRender`/`onChange` can react to fresh state), but the grafted
|
|
24
|
+
content's wrapper VNode was rebuilt from scratch on every replay, handing
|
|
25
|
+
Preact a brand new `ref` closure for a DOM node that was already grafted. That
|
|
26
|
+
churn corrupted Preact's internal DOM bookkeeping for the grafted subtree over
|
|
27
|
+
repeated re-renders, especially in components with ongoing state changes. The
|
|
28
|
+
wrapper VNode is now cached per element, so repeated replays reuse the same
|
|
29
|
+
VNode instance and Preact's diff bails out instead of re-touching the subtree.
|
|
30
|
+
|
|
31
|
+
See `docs/slot-dom-graft-race.md` for the full trace, including why an earlier
|
|
32
|
+
attempt at this fix (making `replaceWith` a no-op after its first run) was
|
|
33
|
+
wrong.
|
|
34
|
+
|
|
35
|
+
## 2.1.0-beta.4
|
|
36
|
+
|
|
37
|
+
### Patch Changes
|
|
38
|
+
|
|
39
|
+
- 8fc53e5: Fix `Slot` silently dropping
|
|
40
|
+
`replaceWith`/`appendChild`/`prependChild`/
|
|
41
|
+
`appendSibling`/`prependSibling`/`remove` calls made from a `slot` callback
|
|
42
|
+
that doesn't `return`/`await` its own async work (e.g. a non-`async` callback
|
|
43
|
+
that calls an `async` helper as a bare statement). Previously, nothing was
|
|
44
|
+
left to trigger the render that would have picked up the queued method once
|
|
45
|
+
the slot's normal init-triggered render pass had already completed. `Slot` now
|
|
46
|
+
detects that case and flushes the pending update itself. Existing callers that
|
|
47
|
+
already `return`/`await` their `slot` callback are unaffected.
|
|
48
|
+
|
|
49
|
+
See `docs/slot-dom-graft-race.md` for the full trace.
|
|
50
|
+
|
|
3
51
|
## 2.1.0-beta.3
|
|
4
52
|
|
|
5
53
|
### Patch Changes
|
package/package.json
CHANGED
package/src/lib/slot.tsx
CHANGED
|
@@ -146,9 +146,21 @@ export function useSlot<K, V extends HTMLElement>(
|
|
|
146
146
|
});
|
|
147
147
|
};
|
|
148
148
|
|
|
149
|
+
const handleLifeCycleRenderRef = useRef<() => Promise<void>>();
|
|
150
|
+
|
|
149
151
|
const _registerMethod = useCallback((cb: Function) => {
|
|
150
152
|
if (typeof cb === 'function') {
|
|
151
153
|
methodsRef.current.push(cb);
|
|
154
|
+
|
|
155
|
+
// If the slot already finished its normal init-triggered render pass
|
|
156
|
+
// (status is back to 'ready'), nothing else will flush this method.
|
|
157
|
+
// This happens when an async `slot` callback doesn't `return`/`await`
|
|
158
|
+
// its own async work before calling replaceWith/appendChild/etc. --
|
|
159
|
+
// `handleLifeCycleInit` moves on before this method gets registered.
|
|
160
|
+
// Flush it ourselves rather than silently dropping it.
|
|
161
|
+
if (status.current === 'ready') {
|
|
162
|
+
handleLifeCycleRenderRef.current?.();
|
|
163
|
+
}
|
|
152
164
|
} else {
|
|
153
165
|
console.warn('Skipped: Invalid _registerMethod', cb);
|
|
154
166
|
}
|
|
@@ -157,20 +169,59 @@ export function useSlot<K, V extends HTMLElement>(
|
|
|
157
169
|
// @ts-ignore
|
|
158
170
|
context._registerMethod = _registerMethod;
|
|
159
171
|
|
|
172
|
+
// `onRender`-style registered methods are replayed on every
|
|
173
|
+
// `handleLifeCycleRender` pass (see `_registerMethod`/`methodsRef` above),
|
|
174
|
+
// and `handleLifeCycleRender` always resets `children` to the fallback
|
|
175
|
+
// before replaying them -- so `replaceWith`/`appendChild`/`prependChild`
|
|
176
|
+
// must genuinely re-apply their VNode on every replay, not just once.
|
|
177
|
+
// Without caching, each replay would call `createElement` fresh, handing
|
|
178
|
+
// Preact a brand new VNode (and new `ref` identity) for content that's
|
|
179
|
+
// already grafted, forcing it to detach/reattach the `ref` for no reason.
|
|
180
|
+
// Caching per `elem` means repeat calls return the *same* VNode object;
|
|
181
|
+
// Preact's diff treats `oldVNode === newVNode` as an explicit "unchanged,
|
|
182
|
+
// skip" signal, so it never re-touches the grafted subtree. See
|
|
183
|
+
// docs/slot-dom-graft-race.md.
|
|
184
|
+
const htmlElementVNodeCache = useMemo(
|
|
185
|
+
() => new Map<HTMLElement, VNode<any>>(),
|
|
186
|
+
// `contentTag` isn't read by the factory -- it's a deliberate cache-bust
|
|
187
|
+
// key, so a `contentTag` change starts a fresh cache instead of handing
|
|
188
|
+
// out VNodes built for the old wrapper tag.
|
|
189
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
190
|
+
[contentTag],
|
|
191
|
+
);
|
|
192
|
+
|
|
160
193
|
const _htmlElementToVNode = useCallback(
|
|
161
194
|
(elem: HTMLElement) => {
|
|
162
|
-
|
|
195
|
+
const cached = htmlElementVNodeCache.get(elem);
|
|
196
|
+
if (cached) return cached;
|
|
197
|
+
|
|
198
|
+
const vnode = createElement(
|
|
163
199
|
contentTag,
|
|
164
200
|
{
|
|
165
201
|
'data-slot-html-element': elem.tagName.toLowerCase(),
|
|
166
202
|
ref: (refElem: HTMLElement | null): void => {
|
|
167
|
-
refElem
|
|
203
|
+
if (refElem) {
|
|
204
|
+
refElem.appendChild(elem);
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Preact calls `ref` with `null` when this specific VNode is
|
|
209
|
+
// detached -- either superseded by a different element at the
|
|
210
|
+
// same position (a later replaceWith/appendChild/prependChild
|
|
211
|
+
// call) or because the Slot unmounted entirely. Either way,
|
|
212
|
+
// `elem` is no longer part of the tree, so drop it from the
|
|
213
|
+
// cache now instead of holding onto it (and the DOM node it
|
|
214
|
+
// wraps) for the Slot's remaining lifetime.
|
|
215
|
+
htmlElementVNodeCache.delete(elem);
|
|
168
216
|
},
|
|
169
217
|
},
|
|
170
218
|
null,
|
|
171
219
|
);
|
|
220
|
+
|
|
221
|
+
htmlElementVNodeCache.set(elem, vnode);
|
|
222
|
+
return vnode;
|
|
172
223
|
},
|
|
173
|
-
[contentTag],
|
|
224
|
+
[contentTag, htmlElementVNodeCache],
|
|
174
225
|
);
|
|
175
226
|
|
|
176
227
|
// @ts-ignore
|
|
@@ -337,6 +388,8 @@ export function useSlot<K, V extends HTMLElement>(
|
|
|
337
388
|
status.current = 'ready';
|
|
338
389
|
}, [children, context, name, props, render, state]);
|
|
339
390
|
|
|
391
|
+
handleLifeCycleRenderRef.current = handleLifeCycleRender;
|
|
392
|
+
|
|
340
393
|
// Initialization
|
|
341
394
|
const handleLifeCycleInit = useCallback(async () => {
|
|
342
395
|
if (!callback) return;
|