@reckona/mreact-compat 0.0.164 → 0.0.165

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.
@@ -27,13 +27,15 @@ import {
27
27
  import { applyPostChildFormProps, applyProps } from "./dom-props.js";
28
28
  import { syncChildNodes, syncOwnedChildNodes, syncScopedChildNodes } from "./dom-children.js";
29
29
  import { setLogicalEventParent } from "./host-event-binder.js";
30
- import { NoFlags, Placement, Update } from "./fiber-flags.js";
30
+ import { ChildDeletion, NoFlags, Placement, Update } from "./fiber-flags.js";
31
31
  import {
32
32
  createHostElement,
33
33
  hostElementMatches,
34
+ isDomHostElement,
34
35
  isHostElement,
35
36
  namespaceForHostChildren,
36
37
  namespaceForHostElement,
38
+ type CustomHostDocument,
37
39
  type HostNamespace,
38
40
  } from "./dom-host-rules.js";
39
41
  import { createFiber, createWorkInProgress, type Fiber, type FiberRoot } from "./fiber.js";
@@ -90,13 +92,14 @@ interface SuspenseFiberState {
90
92
  }
91
93
 
92
94
  const committedPortalContainers = new Set<Element>();
95
+ const pendingHostRefUpdates: { ref: unknown; node: unknown }[] = [];
93
96
 
94
97
  interface FiberHydrationOptions extends RenderOptions {
95
98
  previousNodes?: readonly Node[];
96
99
  resumeId?: string;
97
100
  consumeResumeMarkers?: boolean;
98
101
  namespace?: HostNamespace;
99
- documentRef?: Document;
102
+ documentRef?: Document | CustomHostDocument;
100
103
  }
101
104
 
102
105
  const SKIP_COMMIT_PATH = "\0";
@@ -107,6 +110,11 @@ interface FiberReconcileResult {
107
110
  consumed: number;
108
111
  }
109
112
 
113
+ interface AppendSuffixCommitHint {
114
+ fiber: Fiber;
115
+ index: number;
116
+ }
117
+
110
118
  interface ReactSuspenseBoundary {
111
119
  previousNodes?: Node[];
112
120
  consumed: number;
@@ -241,11 +249,14 @@ export function commitHostFiberRoot(
241
249
  options: RenderOptions = {},
242
250
  ): void {
243
251
  runWithHostCommit(() => {
252
+ let committed = false;
244
253
  try {
245
254
  committedPortalContainers.clear();
255
+ pendingHostRefUpdates.length = 0;
246
256
  const commitPath = getRootCommitPath(options);
247
257
  if (!hasChildListMutation(finishedWork)) {
248
258
  commitHostDirtyChildren(finishedWork.child, root.container, root.container, commitPath, options);
259
+ committed = true;
249
260
  return;
250
261
  }
251
262
 
@@ -254,12 +265,19 @@ export function commitHostFiberRoot(
254
265
  finishedWork.subtreeChildListChanged &&
255
266
  commitHostKeyedChildListMutation(finishedWork.child, root.container, root.container, commitPath, options)
256
267
  ) {
268
+ committed = true;
257
269
  return;
258
270
  }
259
271
 
260
272
  const nodes = commitHostChildren(finishedWork.child, root.container, root.container, commitPath, options);
261
273
  syncChildNodes(root.container, nodes);
274
+ committed = true;
262
275
  } finally {
276
+ if (committed) {
277
+ flushPendingHostRefUpdates();
278
+ } else {
279
+ pendingHostRefUpdates.length = 0;
280
+ }
263
281
  committedPortalContainers.clear();
264
282
  }
265
283
  });
@@ -272,12 +290,20 @@ export function commitHydratingHostFiberRoot(
272
290
  options: FiberHydrationOptions = {},
273
291
  ): void {
274
292
  runWithHostCommit(() => {
293
+ let committed = false;
275
294
  try {
276
295
  committedPortalContainers.clear();
296
+ pendingHostRefUpdates.length = 0;
277
297
  const eventRoot = root.container;
278
298
  const nodes = commitHostChildren(finishedWork.child, scope.parent, eventRoot, "", options);
279
299
  syncScopedChildNodes(scope.parent, scope.before, scope.after, nodes);
300
+ committed = true;
280
301
  } finally {
302
+ if (committed) {
303
+ flushPendingHostRefUpdates();
304
+ } else {
305
+ pendingHostRefUpdates.length = 0;
306
+ }
281
307
  committedPortalContainers.clear();
282
308
  }
283
309
  });
@@ -303,6 +329,9 @@ function reconcileHostChild(
303
329
 
304
330
  if (node === null || node === undefined || typeof node === "boolean") {
305
331
  parent.childListChanged = currentFirstChild !== undefined;
332
+ if (currentFirstChild !== undefined) {
333
+ markOptimizedChildrenForDeletion(parent, currentFirstChild);
334
+ }
306
335
  return { fiber: undefined, consumed: 0 };
307
336
  }
308
337
 
@@ -324,6 +353,8 @@ function reconcileHostChild(
324
353
  let previous: Fiber | undefined;
325
354
  let consumed = 0;
326
355
  let skipRemainingKeyedLookup = false;
356
+ const usedCurrentChildren =
357
+ currentFirstChild === undefined ? undefined : new Set<Fiber>();
327
358
 
328
359
  for (let index = 0; index < childCount; index += 1) {
329
360
  const child = children === undefined ? node : children[index];
@@ -376,9 +407,20 @@ function reconcileHostChild(
376
407
  const fiber = result.fiber;
377
408
 
378
409
  if (fiber === undefined) {
410
+ if (matchedCurrent !== undefined) {
411
+ usedCurrentChildren?.add(matchedCurrent);
412
+ markOptimizedChildForDeletion(parent, matchedCurrent);
413
+ }
379
414
  continue;
380
415
  }
381
416
 
417
+ if (matchedCurrent !== undefined) {
418
+ usedCurrentChildren?.add(matchedCurrent);
419
+ if (fiber !== matchedCurrent && fiber.alternate !== matchedCurrent) {
420
+ markOptimizedChildForDeletion(parent, matchedCurrent);
421
+ }
422
+ }
423
+
382
424
  if (key === undefined) {
383
425
  currentUnkeyed = currentUnkeyed?.sibling;
384
426
  }
@@ -407,6 +449,7 @@ function reconcileHostChild(
407
449
  previous = fiber;
408
450
  }
409
451
 
452
+ markUnusedCurrentChildrenForDeletion(parent, currentFirstChild, usedCurrentChildren);
410
453
  parent.childListChanged = childFiberListShapeChanged(currentFirstChild, first);
411
454
 
412
455
  return { fiber: first, consumed };
@@ -435,6 +478,7 @@ function reconcileKeyedRowHostChildren(
435
478
  let subtreeFlags = NoFlags;
436
479
  let subtreeChildListChanged = false;
437
480
  let hasRefSubtree = false;
481
+ let appendSuffix: AppendSuffixCommitHint | undefined;
438
482
  const canReuseUnchangedRows = hasSameKeyOrderPrefix(currentFirstChild, children);
439
483
  const row = createKeyedRowHostElementScratch();
440
484
 
@@ -446,12 +490,14 @@ function reconcileKeyedRowHostChildren(
446
490
  }
447
491
 
448
492
  let matchedCurrent: Fiber | undefined;
493
+ let matchedByAppendSuffix = false;
449
494
 
450
495
  if (skipRemainingKeyedLookup) {
451
496
  matchedCurrent = undefined;
452
497
  } else if (currentKeyed === undefined) {
453
498
  listShapeChanged = true;
454
499
  skipRemainingKeyedLookup = true;
500
+ matchedByAppendSuffix = true;
455
501
  matchedCurrent = undefined;
456
502
  } else if (currentKeyed?.key === row.key) {
457
503
  matchedCurrent = currentKeyed;
@@ -460,11 +506,15 @@ function reconcileKeyedRowHostChildren(
460
506
  currentKeyed?.sibling?.key === row.key &&
461
507
  canSkipSingleDeletedKeyedFiber(children, index, currentKeyed.sibling)
462
508
  ) {
509
+ const deleted = currentKeyed;
510
+ const matched = currentKeyed.sibling;
463
511
  listShapeChanged = true;
464
- matchedCurrent = currentKeyed.sibling;
465
- currentKeyed = currentKeyed.sibling.sibling;
512
+ markOptimizedChildForDeletion(parent, deleted);
513
+ matchedCurrent = matched;
514
+ currentKeyed = matched.sibling;
466
515
  } else if (canSkipRemainingKeyedLookup(currentKeyed, children, index)) {
467
516
  listShapeChanged = true;
517
+ markOptimizedChildrenForDeletion(parent, currentKeyed);
468
518
  skipRemainingKeyedLookup = true;
469
519
  currentKeyed = undefined;
470
520
  matchedCurrent = undefined;
@@ -478,6 +528,10 @@ function reconcileKeyedRowHostChildren(
478
528
  : (canReuseUnchangedRows ? getReusableKeyedRowHostFiber(matchedCurrent, row) : undefined) ??
479
529
  createKeyedRowHostFiber(parent, matchedCurrent, row, options);
480
530
 
531
+ if (matchedByAppendSuffix && appendSuffix === undefined) {
532
+ appendSuffix = { fiber, index };
533
+ }
534
+
481
535
  if (first === undefined) {
482
536
  first = fiber;
483
537
  } else if (previous !== undefined) {
@@ -502,15 +556,55 @@ function reconcileKeyedRowHostChildren(
502
556
 
503
557
  if (currentKeyed !== undefined) {
504
558
  listShapeChanged = true;
559
+ markOptimizedChildrenForDeletion(parent, currentKeyed);
505
560
  }
506
561
 
507
562
  parent.hasRefSubtree = hasRefSubtree;
508
563
  parent.subtreeFlags = subtreeFlags;
509
564
  parent.subtreeChildListChanged = subtreeChildListChanged;
510
565
  parent.childListChanged = listShapeChanged;
566
+ if (appendSuffix !== undefined && canStoreAppendSuffixCommitHint(parent)) {
567
+ parent.memoizedState = appendSuffix;
568
+ }
511
569
  return { fiber: first, consumed: 0 };
512
570
  }
513
571
 
572
+ function canStoreAppendSuffixCommitHint(parent: Fiber): boolean {
573
+ return (
574
+ parent.tag === "fragment" ||
575
+ parent.tag === "host-component" ||
576
+ parent.tag === "host-root"
577
+ );
578
+ }
579
+
580
+ function markOptimizedChildForDeletion(parent: Fiber, _child: Fiber): void {
581
+ parent.flags |= ChildDeletion;
582
+ }
583
+
584
+ function markOptimizedChildrenForDeletion(parent: Fiber, _firstChild: Fiber): void {
585
+ parent.flags |= ChildDeletion;
586
+ }
587
+
588
+ function markUnusedCurrentChildrenForDeletion(
589
+ parent: Fiber,
590
+ firstChild: Fiber | undefined,
591
+ used: ReadonlySet<Fiber> | undefined,
592
+ ): void {
593
+ if (firstChild === undefined || used === undefined) {
594
+ return;
595
+ }
596
+
597
+ let cursor: Fiber | undefined = firstChild;
598
+
599
+ while (cursor !== undefined) {
600
+ if (!used.has(cursor)) {
601
+ parent.flags |= ChildDeletion;
602
+ return;
603
+ }
604
+ cursor = cursor.sibling;
605
+ }
606
+ }
607
+
514
608
  function hasSameKeyOrderPrefix(
515
609
  currentFirstChild: Fiber,
516
610
  children: readonly ReactCompatNode[],
@@ -927,7 +1021,7 @@ function createHostFiberImpl(
927
1021
  ? existing
928
1022
  : current?.tag === "host-text" && current.stateNode instanceof Text
929
1023
  ? current.stateNode
930
- : getDocumentRef(options).createTextNode("");
1024
+ : createHostTextNode(getDocumentRef(options));
931
1025
 
932
1026
  if (existing instanceof Text && existing.data !== String(node)) {
933
1027
  reportRecoverable(
@@ -1627,13 +1721,12 @@ function commitHostDirtyFiber(
1627
1721
  fiber.hydrateExisting !== true &&
1628
1722
  isRowTextOnlyUpdate(fiber.memoizedProps, props);
1629
1723
 
1630
- if (!propsAreUnchanged && !propsAreChildrenOnly && !textOnlyRowUpdate) {
1724
+ if (isDomHostElement(element) && !propsAreUnchanged && !propsAreChildrenOnly && !textOnlyRowUpdate) {
1631
1725
  applyProps(element, props, path, {
1632
1726
  ...options,
1633
1727
  eventRoot,
1634
1728
  preserveHydrationAttributes: fiber.hydrateExisting,
1635
1729
  });
1636
- applyChangedRef(previousProps?.ref, props.ref, element);
1637
1730
  }
1638
1731
 
1639
1732
  if (directTextChild !== undefined) {
@@ -1646,16 +1739,19 @@ function commitHostDirtyFiber(
1646
1739
  ) {
1647
1740
  const childNodes = commitHostChildren(fiber.child, element, eventRoot, `${path}.c`, options);
1648
1741
  if (
1649
- !(childNodes.length === 0 && committedPortalContainers.has(element)) &&
1650
- !shouldPreserveContentEditableChildren(element, props, childNodes)
1742
+ !(isDomHostElement(element) && childNodes.length === 0 && committedPortalContainers.has(element)) &&
1743
+ !(isDomHostElement(element) && shouldPreserveContentEditableChildren(element, props, childNodes))
1651
1744
  ) {
1652
- syncChildNodes(element, childNodes);
1745
+ syncChildNodes(element as ParentNode, childNodes);
1653
1746
  }
1654
1747
  } else if (fiber.subtreeFlags !== NoFlags) {
1655
1748
  commitHostDirtyChildren(fiber.child, element, eventRoot, `${path}.c`, options);
1656
1749
  }
1657
1750
 
1658
- applyPostChildFormProps(element, props, previousProps);
1751
+ if (isDomHostElement(element)) {
1752
+ applyPostChildFormProps(element, props, previousProps);
1753
+ }
1754
+ applyChangedRef(previousProps?.ref, props.ref, element);
1659
1755
  fiber.memoizedProps = props;
1660
1756
  finishCommittedFiber(fiber);
1661
1757
  return;
@@ -1664,9 +1760,42 @@ function commitHostDirtyFiber(
1664
1760
  if (fiber.tag === "portal") {
1665
1761
  const container = fiber.stateNode;
1666
1762
 
1667
- if (container instanceof Element) {
1668
- setLogicalEventParent(container, parent);
1669
- commitHostDirtyChildren(fiber.child, container, container, `${path}.portal`, options);
1763
+ if (isPortalHostContainer(container)) {
1764
+ if (container instanceof Element) {
1765
+ setLogicalEventParent(container, parent);
1766
+ }
1767
+ const portalEventRoot =
1768
+ container instanceof Element && eventRoot !== container && eventRoot.contains(container)
1769
+ ? eventRoot
1770
+ : container instanceof Element
1771
+ ? container
1772
+ : eventRoot;
1773
+ const portalOptions = withPortalDocumentRef(options, container);
1774
+
1775
+ if (
1776
+ fiber.childListChanged ||
1777
+ fiber.subtreeChildListChanged ||
1778
+ (fiber.subtreeFlags & Placement) !== NoFlags
1779
+ ) {
1780
+ const childNodes = commitHostChildren(
1781
+ fiber.child,
1782
+ container as ParentNode,
1783
+ portalEventRoot,
1784
+ `${path}.portal`,
1785
+ portalOptions,
1786
+ );
1787
+ const previousNodes = committedHostNodesFromState(fiber.alternate?.memoizedState);
1788
+ syncOwnedChildNodes(container as ParentNode, previousNodes, childNodes);
1789
+ fiber.memoizedState = childNodes;
1790
+ } else {
1791
+ commitHostDirtyChildren(
1792
+ fiber.child,
1793
+ container as ParentNode,
1794
+ portalEventRoot,
1795
+ `${path}.portal`,
1796
+ portalOptions,
1797
+ );
1798
+ }
1670
1799
  }
1671
1800
  fiber.memoizedProps = fiber.pendingProps;
1672
1801
  finishCommittedFiber(fiber);
@@ -1737,6 +1866,11 @@ function commitHostKeyedChildListMutationFiber(
1737
1866
  path: string,
1738
1867
  options: RenderOptions = {},
1739
1868
  ): boolean {
1869
+ if (fiber.tag === "portal") {
1870
+ commitHostDirtyFiber(fiber, parent, eventRoot, path, options);
1871
+ return true;
1872
+ }
1873
+
1740
1874
  if (fiber.childListChanged) {
1741
1875
  const mutationParent =
1742
1876
  fiber.tag === "host-component" && isHostElement(fiber.stateNode)
@@ -1793,17 +1927,28 @@ function commitHostAppendSuffix(
1793
1927
  path: string,
1794
1928
  options: RenderOptions,
1795
1929
  ): boolean {
1796
- const append = getAppendSuffix(fiber.alternate?.child, fiber.child);
1930
+ const appendHint = readAppendSuffixCommitHint(fiber.memoizedState);
1931
+ const append = appendHint ?? getAppendSuffix(fiber.alternate?.child, fiber.child);
1797
1932
 
1798
1933
  if (append === undefined) {
1799
1934
  return false;
1800
1935
  }
1801
1936
 
1937
+ if (appendHint !== undefined) {
1938
+ fiber.memoizedState = undefined;
1939
+ }
1940
+
1802
1941
  let cursor: Fiber | undefined = append.fiber;
1803
1942
  let index = append.index;
1804
1943
 
1805
1944
  while (cursor !== undefined) {
1806
- for (const node of commitHostFiber(cursor, parent, eventRoot, joinCommitPath(path, String(index)), options)) {
1945
+ for (const node of commitHostFiber(
1946
+ cursor,
1947
+ parent,
1948
+ eventRoot,
1949
+ joinCommitPath(path, String(index)),
1950
+ options,
1951
+ )) {
1807
1952
  parent.appendChild(node);
1808
1953
  }
1809
1954
  cursor = cursor.sibling;
@@ -1813,6 +1958,17 @@ function commitHostAppendSuffix(
1813
1958
  return true;
1814
1959
  }
1815
1960
 
1961
+ function readAppendSuffixCommitHint(value: unknown): AppendSuffixCommitHint | undefined {
1962
+ if (typeof value !== "object" || value === null) {
1963
+ return undefined;
1964
+ }
1965
+
1966
+ const candidate = value as Partial<AppendSuffixCommitHint>;
1967
+ return candidate.fiber !== undefined && typeof candidate.index === "number"
1968
+ ? { fiber: candidate.fiber, index: candidate.index }
1969
+ : undefined;
1970
+ }
1971
+
1816
1972
  function commitHostSingleRemoval(fiber: Fiber, parent: ParentNode): boolean {
1817
1973
  const removed = getSingleRemovedFiber(fiber.alternate?.child, fiber.child);
1818
1974
 
@@ -2016,13 +2172,12 @@ function commitHostFiber(
2016
2172
  fiber.hydrateExisting !== true &&
2017
2173
  isRowTextOnlyUpdate(fiber.memoizedProps, props);
2018
2174
 
2019
- if (!propsAreUnchanged && !propsAreChildrenOnly && !textOnlyRowUpdate) {
2175
+ if (isDomHostElement(element) && !propsAreUnchanged && !propsAreChildrenOnly && !textOnlyRowUpdate) {
2020
2176
  applyProps(element, props, path, {
2021
2177
  ...options,
2022
2178
  eventRoot,
2023
2179
  preserveHydrationAttributes: fiber.hydrateExisting,
2024
2180
  });
2025
- applyChangedRef(previousProps?.ref, props.ref, element);
2026
2181
  }
2027
2182
  if (directTextChild !== undefined) {
2028
2183
  const text = syncDirectHostTextChild(element, directTextChild);
@@ -2036,16 +2191,19 @@ function commitHostFiber(
2036
2191
  ) {
2037
2192
  const childNodes = commitHostChildren(fiber.child, element, eventRoot, `${path}.c`, options);
2038
2193
  if (
2039
- !(childNodes.length === 0 && committedPortalContainers.has(element)) &&
2040
- !shouldPreserveContentEditableChildren(element, props, childNodes)
2194
+ !(isDomHostElement(element) && childNodes.length === 0 && committedPortalContainers.has(element)) &&
2195
+ !(isDomHostElement(element) && shouldPreserveContentEditableChildren(element, props, childNodes))
2041
2196
  ) {
2042
- syncChildNodes(element, childNodes);
2197
+ syncChildNodes(element as ParentNode, childNodes);
2043
2198
  }
2044
2199
  } else if (fiber.subtreeFlags !== NoFlags) {
2045
2200
  commitHostChildren(fiber.child, element, eventRoot, `${path}.c`, options);
2046
2201
  }
2047
2202
 
2048
- applyPostChildFormProps(element, props, previousProps);
2203
+ if (isDomHostElement(element)) {
2204
+ applyPostChildFormProps(element, props, previousProps);
2205
+ }
2206
+ applyChangedRef(previousProps?.ref, props.ref, element);
2049
2207
  fiber.memoizedProps = props;
2050
2208
  finishCommittedFiber(fiber);
2051
2209
  return [element];
@@ -2138,25 +2296,30 @@ function commitHostFiber(
2138
2296
  if (fiber.tag === "portal") {
2139
2297
  const container = fiber.stateNode;
2140
2298
 
2141
- if (!(container instanceof Element)) {
2299
+ if (!isPortalHostContainer(container)) {
2142
2300
  return [];
2143
2301
  }
2144
2302
 
2145
- setLogicalEventParent(container, parent);
2146
- committedPortalContainers.add(container);
2303
+ if (container instanceof Element) {
2304
+ setLogicalEventParent(container, parent);
2305
+ committedPortalContainers.add(container);
2306
+ }
2147
2307
  const portalEventRoot =
2148
- eventRoot !== container && eventRoot.contains(container) ? eventRoot : container;
2308
+ container instanceof Element && eventRoot !== container && eventRoot.contains(container)
2309
+ ? eventRoot
2310
+ : container instanceof Element
2311
+ ? container
2312
+ : eventRoot;
2313
+ const portalOptions = withPortalDocumentRef(options, container);
2149
2314
  const childNodes = commitHostChildren(
2150
2315
  fiber.child,
2151
- container,
2316
+ container as ParentNode,
2152
2317
  portalEventRoot,
2153
2318
  `${path}.portal`,
2154
- options,
2319
+ portalOptions,
2155
2320
  );
2156
- const previousNodes = Array.isArray(fiber.alternate?.memoizedState)
2157
- ? fiber.alternate.memoizedState.filter((node): node is Node => node instanceof Node)
2158
- : [];
2159
- syncOwnedChildNodes(container, previousNodes, childNodes);
2321
+ const previousNodes = committedHostNodesFromState(fiber.alternate?.memoizedState);
2322
+ syncOwnedChildNodes(container as ParentNode, previousNodes, childNodes);
2160
2323
  fiber.memoizedState = childNodes;
2161
2324
  fiber.memoizedProps = fiber.pendingProps;
2162
2325
  finishCommittedFiber(fiber);
@@ -2968,10 +3131,18 @@ function normalizeChildren(node: ReactCompatNode): ReactCompatNode[] {
2968
3131
  return Array.isArray(node) ? node : [node];
2969
3132
  }
2970
3133
 
2971
- function getDocumentRef(options: FiberHydrationOptions): Document {
3134
+ function getDocumentRef(options: FiberHydrationOptions): Document | CustomHostDocument {
2972
3135
  return options.documentRef ?? document;
2973
3136
  }
2974
3137
 
3138
+ function createHostTextNode(documentRef: Document | CustomHostDocument): Text {
3139
+ if ("createTextNode" in documentRef && typeof documentRef.createTextNode === "function") {
3140
+ return documentRef.createTextNode("");
3141
+ }
3142
+
3143
+ return document.createTextNode("");
3144
+ }
3145
+
2975
3146
  function collectExistingKeyedFibers(
2976
3147
  firstChild: Fiber | undefined,
2977
3148
  ): Map<string, Fiber> {
@@ -3184,6 +3355,64 @@ function applyChangedRef(previousRef: unknown, nextRef: unknown, node: unknown):
3184
3355
  return;
3185
3356
  }
3186
3357
 
3187
- applyRef(previousRef, null);
3188
- applyRef(nextRef, node);
3358
+ queueHostRefUpdate(previousRef, null);
3359
+ queueHostRefUpdate(nextRef, node);
3360
+ }
3361
+
3362
+ function queueHostRefUpdate(ref: unknown, node: unknown): void {
3363
+ if (ref === null || ref === undefined) {
3364
+ return;
3365
+ }
3366
+
3367
+ pendingHostRefUpdates.push({ ref, node });
3368
+ }
3369
+
3370
+ function flushPendingHostRefUpdates(): void {
3371
+ const pending = pendingHostRefUpdates.splice(0);
3372
+ for (const { ref, node } of pending) {
3373
+ applyRef(ref, node);
3374
+ }
3375
+ }
3376
+
3377
+ function isPortalHostContainer(value: unknown): value is ParentNode {
3378
+ if (value instanceof Element) {
3379
+ return true;
3380
+ }
3381
+
3382
+ if (typeof value !== "object" || value === null) {
3383
+ return false;
3384
+ }
3385
+
3386
+ const candidate = value as Partial<ParentNode> & {
3387
+ ownerDocument?: { createElement?: unknown };
3388
+ };
3389
+ return (
3390
+ typeof candidate.appendChild === "function" &&
3391
+ typeof candidate.insertBefore === "function" &&
3392
+ typeof candidate.removeChild === "function" &&
3393
+ typeof candidate.ownerDocument?.createElement === "function"
3394
+ );
3395
+ }
3396
+
3397
+ function withPortalDocumentRef(
3398
+ options: RenderOptions,
3399
+ container: ParentNode,
3400
+ ): RenderOptions & { documentRef?: Document | CustomHostDocument } {
3401
+ const ownerDocument = (container as { ownerDocument?: unknown }).ownerDocument;
3402
+ if (
3403
+ typeof ownerDocument === "object" &&
3404
+ ownerDocument !== null &&
3405
+ typeof (ownerDocument as { createElement?: unknown }).createElement === "function"
3406
+ ) {
3407
+ return {
3408
+ ...options,
3409
+ documentRef: ownerDocument as Document | CustomHostDocument,
3410
+ };
3411
+ }
3412
+
3413
+ return options;
3414
+ }
3415
+
3416
+ function committedHostNodesFromState(state: unknown): Node[] {
3417
+ return Array.isArray(state) ? state as Node[] : [];
3189
3418
  }
package/src/root.ts CHANGED
@@ -6,7 +6,7 @@ import {
6
6
  type RenderPriority,
7
7
  type RootRuntime,
8
8
  } from "./hooks.js";
9
- import { removeChildIfPresent } from "./dom-children.js";
9
+ import { collectOwnedChildNodes, removeChildIfPresent } from "./dom-children.js";
10
10
  import { commitDevToolsRoot, unmountDevToolsRoot } from "./devtools.js";
11
11
  import {
12
12
  applyStreamingHydrationFragments,
@@ -532,7 +532,8 @@ function removeStalePortalNodes(
532
532
  snapshot: PortalRenderSnapshot,
533
533
  runtime: RootRuntime,
534
534
  ): void {
535
- for (const [container, nodes] of snapshot.nodes) {
535
+ for (const container of snapshot.containers) {
536
+ const nodes = snapshot.nodes.get(container) ?? new Set(collectOwnedChildNodes(container));
536
537
  const currentNodes = runtime.portalNodes.get(container);
537
538
 
538
539
  for (const node of nodes) {