@reckona/mreact-compat 0.0.163 → 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,10 +478,8 @@ function reconcileKeyedRowHostChildren(
435
478
  let subtreeFlags = NoFlags;
436
479
  let subtreeChildListChanged = false;
437
480
  let hasRefSubtree = false;
438
- const currentSiblingCount = countFiberSiblings(currentFirstChild);
439
- const canReuseUnchangedRows =
440
- currentSiblingCount === children.length ||
441
- isKeyedAppendOnly(currentFirstChild, children, currentSiblingCount);
481
+ let appendSuffix: AppendSuffixCommitHint | undefined;
482
+ const canReuseUnchangedRows = hasSameKeyOrderPrefix(currentFirstChild, children);
442
483
  const row = createKeyedRowHostElementScratch();
443
484
 
444
485
  for (let index = 0; index < children.length; index += 1) {
@@ -449,12 +490,14 @@ function reconcileKeyedRowHostChildren(
449
490
  }
450
491
 
451
492
  let matchedCurrent: Fiber | undefined;
493
+ let matchedByAppendSuffix = false;
452
494
 
453
495
  if (skipRemainingKeyedLookup) {
454
496
  matchedCurrent = undefined;
455
497
  } else if (currentKeyed === undefined) {
456
498
  listShapeChanged = true;
457
499
  skipRemainingKeyedLookup = true;
500
+ matchedByAppendSuffix = true;
458
501
  matchedCurrent = undefined;
459
502
  } else if (currentKeyed?.key === row.key) {
460
503
  matchedCurrent = currentKeyed;
@@ -463,11 +506,15 @@ function reconcileKeyedRowHostChildren(
463
506
  currentKeyed?.sibling?.key === row.key &&
464
507
  canSkipSingleDeletedKeyedFiber(children, index, currentKeyed.sibling)
465
508
  ) {
509
+ const deleted = currentKeyed;
510
+ const matched = currentKeyed.sibling;
466
511
  listShapeChanged = true;
467
- matchedCurrent = currentKeyed.sibling;
468
- currentKeyed = currentKeyed.sibling.sibling;
512
+ markOptimizedChildForDeletion(parent, deleted);
513
+ matchedCurrent = matched;
514
+ currentKeyed = matched.sibling;
469
515
  } else if (canSkipRemainingKeyedLookup(currentKeyed, children, index)) {
470
516
  listShapeChanged = true;
517
+ markOptimizedChildrenForDeletion(parent, currentKeyed);
471
518
  skipRemainingKeyedLookup = true;
472
519
  currentKeyed = undefined;
473
520
  matchedCurrent = undefined;
@@ -481,6 +528,10 @@ function reconcileKeyedRowHostChildren(
481
528
  : (canReuseUnchangedRows ? getReusableKeyedRowHostFiber(matchedCurrent, row) : undefined) ??
482
529
  createKeyedRowHostFiber(parent, matchedCurrent, row, options);
483
530
 
531
+ if (matchedByAppendSuffix && appendSuffix === undefined) {
532
+ appendSuffix = { fiber, index };
533
+ }
534
+
484
535
  if (first === undefined) {
485
536
  first = fiber;
486
537
  } else if (previous !== undefined) {
@@ -505,42 +556,70 @@ function reconcileKeyedRowHostChildren(
505
556
 
506
557
  if (currentKeyed !== undefined) {
507
558
  listShapeChanged = true;
559
+ markOptimizedChildrenForDeletion(parent, currentKeyed);
508
560
  }
509
561
 
510
562
  parent.hasRefSubtree = hasRefSubtree;
511
563
  parent.subtreeFlags = subtreeFlags;
512
564
  parent.subtreeChildListChanged = subtreeChildListChanged;
513
565
  parent.childListChanged = listShapeChanged;
566
+ if (appendSuffix !== undefined && canStoreAppendSuffixCommitHint(parent)) {
567
+ parent.memoizedState = appendSuffix;
568
+ }
514
569
  return { fiber: first, consumed: 0 };
515
570
  }
516
571
 
517
- function countFiberSiblings(first: Fiber): number {
518
- let count = 0;
519
- let cursor: Fiber | undefined = first;
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;
520
598
 
521
599
  while (cursor !== undefined) {
522
- count += 1;
600
+ if (!used.has(cursor)) {
601
+ parent.flags |= ChildDeletion;
602
+ return;
603
+ }
523
604
  cursor = cursor.sibling;
524
605
  }
525
-
526
- return count;
527
606
  }
528
607
 
529
- function isKeyedAppendOnly(
608
+ function hasSameKeyOrderPrefix(
530
609
  currentFirstChild: Fiber,
531
610
  children: readonly ReactCompatNode[],
532
- currentSiblingCount: number,
533
611
  ): boolean {
534
- if (children.length <= currentSiblingCount) {
535
- return false;
536
- }
537
-
538
612
  let current: Fiber | undefined = currentFirstChild;
539
613
 
540
- for (let index = 0; index < currentSiblingCount; index += 1) {
541
- if (current === undefined || current.key !== getNodeKey(children[index])) {
614
+ for (let index = 0; index < children.length; index += 1) {
615
+ if (current === undefined) {
616
+ return true;
617
+ }
618
+
619
+ if (current.key !== getNodeKey(children[index])) {
542
620
  return false;
543
621
  }
622
+
544
623
  current = current.sibling;
545
624
  }
546
625
 
@@ -942,7 +1021,7 @@ function createHostFiberImpl(
942
1021
  ? existing
943
1022
  : current?.tag === "host-text" && current.stateNode instanceof Text
944
1023
  ? current.stateNode
945
- : getDocumentRef(options).createTextNode("");
1024
+ : createHostTextNode(getDocumentRef(options));
946
1025
 
947
1026
  if (existing instanceof Text && existing.data !== String(node)) {
948
1027
  reportRecoverable(
@@ -1642,13 +1721,12 @@ function commitHostDirtyFiber(
1642
1721
  fiber.hydrateExisting !== true &&
1643
1722
  isRowTextOnlyUpdate(fiber.memoizedProps, props);
1644
1723
 
1645
- if (!propsAreUnchanged && !propsAreChildrenOnly && !textOnlyRowUpdate) {
1724
+ if (isDomHostElement(element) && !propsAreUnchanged && !propsAreChildrenOnly && !textOnlyRowUpdate) {
1646
1725
  applyProps(element, props, path, {
1647
1726
  ...options,
1648
1727
  eventRoot,
1649
1728
  preserveHydrationAttributes: fiber.hydrateExisting,
1650
1729
  });
1651
- applyChangedRef(previousProps?.ref, props.ref, element);
1652
1730
  }
1653
1731
 
1654
1732
  if (directTextChild !== undefined) {
@@ -1661,16 +1739,19 @@ function commitHostDirtyFiber(
1661
1739
  ) {
1662
1740
  const childNodes = commitHostChildren(fiber.child, element, eventRoot, `${path}.c`, options);
1663
1741
  if (
1664
- !(childNodes.length === 0 && committedPortalContainers.has(element)) &&
1665
- !shouldPreserveContentEditableChildren(element, props, childNodes)
1742
+ !(isDomHostElement(element) && childNodes.length === 0 && committedPortalContainers.has(element)) &&
1743
+ !(isDomHostElement(element) && shouldPreserveContentEditableChildren(element, props, childNodes))
1666
1744
  ) {
1667
- syncChildNodes(element, childNodes);
1745
+ syncChildNodes(element as ParentNode, childNodes);
1668
1746
  }
1669
1747
  } else if (fiber.subtreeFlags !== NoFlags) {
1670
1748
  commitHostDirtyChildren(fiber.child, element, eventRoot, `${path}.c`, options);
1671
1749
  }
1672
1750
 
1673
- applyPostChildFormProps(element, props, previousProps);
1751
+ if (isDomHostElement(element)) {
1752
+ applyPostChildFormProps(element, props, previousProps);
1753
+ }
1754
+ applyChangedRef(previousProps?.ref, props.ref, element);
1674
1755
  fiber.memoizedProps = props;
1675
1756
  finishCommittedFiber(fiber);
1676
1757
  return;
@@ -1679,9 +1760,42 @@ function commitHostDirtyFiber(
1679
1760
  if (fiber.tag === "portal") {
1680
1761
  const container = fiber.stateNode;
1681
1762
 
1682
- if (container instanceof Element) {
1683
- setLogicalEventParent(container, parent);
1684
- 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
+ }
1685
1799
  }
1686
1800
  fiber.memoizedProps = fiber.pendingProps;
1687
1801
  finishCommittedFiber(fiber);
@@ -1752,6 +1866,11 @@ function commitHostKeyedChildListMutationFiber(
1752
1866
  path: string,
1753
1867
  options: RenderOptions = {},
1754
1868
  ): boolean {
1869
+ if (fiber.tag === "portal") {
1870
+ commitHostDirtyFiber(fiber, parent, eventRoot, path, options);
1871
+ return true;
1872
+ }
1873
+
1755
1874
  if (fiber.childListChanged) {
1756
1875
  const mutationParent =
1757
1876
  fiber.tag === "host-component" && isHostElement(fiber.stateNode)
@@ -1808,17 +1927,28 @@ function commitHostAppendSuffix(
1808
1927
  path: string,
1809
1928
  options: RenderOptions,
1810
1929
  ): boolean {
1811
- const append = getAppendSuffix(fiber.alternate?.child, fiber.child);
1930
+ const appendHint = readAppendSuffixCommitHint(fiber.memoizedState);
1931
+ const append = appendHint ?? getAppendSuffix(fiber.alternate?.child, fiber.child);
1812
1932
 
1813
1933
  if (append === undefined) {
1814
1934
  return false;
1815
1935
  }
1816
1936
 
1937
+ if (appendHint !== undefined) {
1938
+ fiber.memoizedState = undefined;
1939
+ }
1940
+
1817
1941
  let cursor: Fiber | undefined = append.fiber;
1818
1942
  let index = append.index;
1819
1943
 
1820
1944
  while (cursor !== undefined) {
1821
- 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
+ )) {
1822
1952
  parent.appendChild(node);
1823
1953
  }
1824
1954
  cursor = cursor.sibling;
@@ -1828,6 +1958,17 @@ function commitHostAppendSuffix(
1828
1958
  return true;
1829
1959
  }
1830
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
+
1831
1972
  function commitHostSingleRemoval(fiber: Fiber, parent: ParentNode): boolean {
1832
1973
  const removed = getSingleRemovedFiber(fiber.alternate?.child, fiber.child);
1833
1974
 
@@ -2031,13 +2172,12 @@ function commitHostFiber(
2031
2172
  fiber.hydrateExisting !== true &&
2032
2173
  isRowTextOnlyUpdate(fiber.memoizedProps, props);
2033
2174
 
2034
- if (!propsAreUnchanged && !propsAreChildrenOnly && !textOnlyRowUpdate) {
2175
+ if (isDomHostElement(element) && !propsAreUnchanged && !propsAreChildrenOnly && !textOnlyRowUpdate) {
2035
2176
  applyProps(element, props, path, {
2036
2177
  ...options,
2037
2178
  eventRoot,
2038
2179
  preserveHydrationAttributes: fiber.hydrateExisting,
2039
2180
  });
2040
- applyChangedRef(previousProps?.ref, props.ref, element);
2041
2181
  }
2042
2182
  if (directTextChild !== undefined) {
2043
2183
  const text = syncDirectHostTextChild(element, directTextChild);
@@ -2051,16 +2191,19 @@ function commitHostFiber(
2051
2191
  ) {
2052
2192
  const childNodes = commitHostChildren(fiber.child, element, eventRoot, `${path}.c`, options);
2053
2193
  if (
2054
- !(childNodes.length === 0 && committedPortalContainers.has(element)) &&
2055
- !shouldPreserveContentEditableChildren(element, props, childNodes)
2194
+ !(isDomHostElement(element) && childNodes.length === 0 && committedPortalContainers.has(element)) &&
2195
+ !(isDomHostElement(element) && shouldPreserveContentEditableChildren(element, props, childNodes))
2056
2196
  ) {
2057
- syncChildNodes(element, childNodes);
2197
+ syncChildNodes(element as ParentNode, childNodes);
2058
2198
  }
2059
2199
  } else if (fiber.subtreeFlags !== NoFlags) {
2060
2200
  commitHostChildren(fiber.child, element, eventRoot, `${path}.c`, options);
2061
2201
  }
2062
2202
 
2063
- applyPostChildFormProps(element, props, previousProps);
2203
+ if (isDomHostElement(element)) {
2204
+ applyPostChildFormProps(element, props, previousProps);
2205
+ }
2206
+ applyChangedRef(previousProps?.ref, props.ref, element);
2064
2207
  fiber.memoizedProps = props;
2065
2208
  finishCommittedFiber(fiber);
2066
2209
  return [element];
@@ -2153,25 +2296,30 @@ function commitHostFiber(
2153
2296
  if (fiber.tag === "portal") {
2154
2297
  const container = fiber.stateNode;
2155
2298
 
2156
- if (!(container instanceof Element)) {
2299
+ if (!isPortalHostContainer(container)) {
2157
2300
  return [];
2158
2301
  }
2159
2302
 
2160
- setLogicalEventParent(container, parent);
2161
- committedPortalContainers.add(container);
2303
+ if (container instanceof Element) {
2304
+ setLogicalEventParent(container, parent);
2305
+ committedPortalContainers.add(container);
2306
+ }
2162
2307
  const portalEventRoot =
2163
- 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);
2164
2314
  const childNodes = commitHostChildren(
2165
2315
  fiber.child,
2166
- container,
2316
+ container as ParentNode,
2167
2317
  portalEventRoot,
2168
2318
  `${path}.portal`,
2169
- options,
2319
+ portalOptions,
2170
2320
  );
2171
- const previousNodes = Array.isArray(fiber.alternate?.memoizedState)
2172
- ? fiber.alternate.memoizedState.filter((node): node is Node => node instanceof Node)
2173
- : [];
2174
- syncOwnedChildNodes(container, previousNodes, childNodes);
2321
+ const previousNodes = committedHostNodesFromState(fiber.alternate?.memoizedState);
2322
+ syncOwnedChildNodes(container as ParentNode, previousNodes, childNodes);
2175
2323
  fiber.memoizedState = childNodes;
2176
2324
  fiber.memoizedProps = fiber.pendingProps;
2177
2325
  finishCommittedFiber(fiber);
@@ -2983,10 +3131,18 @@ function normalizeChildren(node: ReactCompatNode): ReactCompatNode[] {
2983
3131
  return Array.isArray(node) ? node : [node];
2984
3132
  }
2985
3133
 
2986
- function getDocumentRef(options: FiberHydrationOptions): Document {
3134
+ function getDocumentRef(options: FiberHydrationOptions): Document | CustomHostDocument {
2987
3135
  return options.documentRef ?? document;
2988
3136
  }
2989
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
+
2990
3146
  function collectExistingKeyedFibers(
2991
3147
  firstChild: Fiber | undefined,
2992
3148
  ): Map<string, Fiber> {
@@ -3199,6 +3355,64 @@ function applyChangedRef(previousRef: unknown, nextRef: unknown, node: unknown):
3199
3355
  return;
3200
3356
  }
3201
3357
 
3202
- applyRef(previousRef, null);
3203
- 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[] : [];
3204
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) {