@reckona/mreact-compat 0.0.91 → 0.0.93

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.
Files changed (104) hide show
  1. package/README.md +1 -0
  2. package/dist/class-component.d.ts +22 -6
  3. package/dist/class-component.d.ts.map +1 -1
  4. package/dist/class-component.js +157 -51
  5. package/dist/class-component.js.map +1 -1
  6. package/dist/context.d.ts +19 -3
  7. package/dist/context.d.ts.map +1 -1
  8. package/dist/context.js +55 -6
  9. package/dist/context.js.map +1 -1
  10. package/dist/dom-children.d.ts +2 -0
  11. package/dist/dom-children.d.ts.map +1 -1
  12. package/dist/dom-children.js +103 -1
  13. package/dist/dom-children.js.map +1 -1
  14. package/dist/dom-host-rules.d.ts +10 -0
  15. package/dist/dom-host-rules.d.ts.map +1 -0
  16. package/dist/dom-host-rules.js +86 -0
  17. package/dist/dom-host-rules.js.map +1 -0
  18. package/dist/dom-props.d.ts +3 -2
  19. package/dist/dom-props.d.ts.map +1 -1
  20. package/dist/dom-props.js +229 -33
  21. package/dist/dom-props.js.map +1 -1
  22. package/dist/element.d.ts +9 -4
  23. package/dist/element.d.ts.map +1 -1
  24. package/dist/element.js +101 -26
  25. package/dist/element.js.map +1 -1
  26. package/dist/event-listeners.d.ts +4 -4
  27. package/dist/event-listeners.d.ts.map +1 -1
  28. package/dist/event-listeners.js +1 -1
  29. package/dist/event-listeners.js.map +1 -1
  30. package/dist/event-types.d.ts +10 -0
  31. package/dist/event-types.d.ts.map +1 -1
  32. package/dist/event-types.js.map +1 -1
  33. package/dist/events.js +22 -1
  34. package/dist/events.js.map +1 -1
  35. package/dist/fiber-commit.d.ts +2 -1
  36. package/dist/fiber-commit.d.ts.map +1 -1
  37. package/dist/fiber-commit.js +13 -1
  38. package/dist/fiber-commit.js.map +1 -1
  39. package/dist/fiber-reconciler.d.ts.map +1 -1
  40. package/dist/fiber-reconciler.js +28 -7
  41. package/dist/fiber-reconciler.js.map +1 -1
  42. package/dist/fiber-work-loop.d.ts.map +1 -1
  43. package/dist/fiber-work-loop.js +4 -3
  44. package/dist/fiber-work-loop.js.map +1 -1
  45. package/dist/fiber.d.ts +5 -0
  46. package/dist/fiber.d.ts.map +1 -1
  47. package/dist/fiber.js +9 -0
  48. package/dist/fiber.js.map +1 -1
  49. package/dist/hooks-entry.d.ts +3 -0
  50. package/dist/hooks-entry.d.ts.map +1 -0
  51. package/dist/hooks-entry.js +2 -0
  52. package/dist/hooks-entry.js.map +1 -0
  53. package/dist/hooks.d.ts +39 -5
  54. package/dist/hooks.d.ts.map +1 -1
  55. package/dist/hooks.js +373 -326
  56. package/dist/hooks.js.map +1 -1
  57. package/dist/host-reconciler.d.ts +3 -0
  58. package/dist/host-reconciler.d.ts.map +1 -1
  59. package/dist/host-reconciler.js +1183 -68
  60. package/dist/host-reconciler.js.map +1 -1
  61. package/dist/hydration.d.ts +1 -1
  62. package/dist/hydration.d.ts.map +1 -1
  63. package/dist/hydration.js.map +1 -1
  64. package/dist/index.d.ts +2 -1
  65. package/dist/index.d.ts.map +1 -1
  66. package/dist/index.js +2 -1
  67. package/dist/index.js.map +1 -1
  68. package/dist/react-default.d.ts +4 -4
  69. package/dist/react-default.d.ts.map +1 -1
  70. package/dist/react-default.js +2 -1
  71. package/dist/react-default.js.map +1 -1
  72. package/dist/reconciler.d.ts.map +1 -1
  73. package/dist/reconciler.js +38 -22
  74. package/dist/reconciler.js.map +1 -1
  75. package/dist/root.d.ts.map +1 -1
  76. package/dist/root.js +48 -13
  77. package/dist/root.js.map +1 -1
  78. package/dist/server-render.d.ts +6 -0
  79. package/dist/server-render.d.ts.map +1 -0
  80. package/dist/server-render.js +307 -0
  81. package/dist/server-render.js.map +1 -0
  82. package/package.json +6 -2
  83. package/src/class-component.ts +313 -51
  84. package/src/context.ts +108 -9
  85. package/src/dom-children.ts +155 -1
  86. package/src/dom-host-rules.ts +115 -0
  87. package/src/dom-props.ts +297 -46
  88. package/src/element.ts +141 -31
  89. package/src/event-listeners.ts +6 -6
  90. package/src/event-types.ts +10 -0
  91. package/src/events.ts +32 -10
  92. package/src/fiber-commit.ts +16 -1
  93. package/src/fiber-reconciler.ts +39 -6
  94. package/src/fiber-work-loop.ts +4 -3
  95. package/src/fiber.ts +14 -0
  96. package/src/hooks-entry.ts +24 -0
  97. package/src/hooks.ts +482 -479
  98. package/src/host-reconciler.ts +1662 -83
  99. package/src/hydration.ts +1 -1
  100. package/src/index.ts +1 -1
  101. package/src/react-default.ts +1 -1
  102. package/src/reconciler.ts +61 -22
  103. package/src/root.ts +55 -12
  104. package/src/server-render.ts +478 -0
@@ -3,6 +3,7 @@ import {
3
3
  ERROR_BOUNDARY_TYPE,
4
4
  FORWARD_REF_TYPE,
5
5
  Fragment,
6
+ HOST_OWN_PROPS_META,
6
7
  LAZY_TYPE,
7
8
  MEMO_TYPE,
8
9
  Profiler,
@@ -16,31 +17,47 @@ import {
16
17
  type ReactCompatNode,
17
18
  } from "./element.js";
18
19
  import {
20
+ consumerContext,
19
21
  isReactCompatConsumer,
20
22
  isReactCompatProvider,
21
23
  renderWithContextProvider,
22
24
  useContext,
23
25
  } from "./context.js";
24
26
  import { applyPostChildFormProps, applyProps } from "./dom-props.js";
25
- import { syncChildNodes, syncScopedChildNodes } from "./dom-children.js";
27
+ import { syncChildNodes, syncOwnedChildNodes, syncScopedChildNodes } from "./dom-children.js";
26
28
  import { setLogicalEventParent } from "./host-event-binder.js";
29
+ import { NoFlags, Placement, Update } from "./fiber-flags.js";
30
+ import {
31
+ createHostElement,
32
+ hostElementMatches,
33
+ isHostElement,
34
+ namespaceForHostChildren,
35
+ namespaceForHostElement,
36
+ type HostNamespace,
37
+ } from "./dom-host-rules.js";
27
38
  import { createFiber, createWorkInProgress, type Fiber, type FiberRoot } from "./fiber.js";
28
39
  import {
29
40
  renderWithRootRuntime,
30
41
  renderWithProfiler,
31
- renderWithStrictMode,
42
+ renderWithStrictModeMemoCapture,
43
+ renderStrictModeReplay,
44
+ runWithHostCommit,
32
45
  restoreRuntimeSnapshot,
33
46
  takeRuntimeSnapshot,
34
47
  getDevToolsHookState,
48
+ hasContextDependency,
49
+ hasChangedContextDependency,
35
50
  type RootRuntime,
36
51
  } from "./hooks.js";
37
52
  import { isThenable } from "./thenable.js";
38
53
  import {
54
+ hasDirtyClassUpdate,
39
55
  isClassComponentType,
40
56
  recoverClassComponentError,
41
57
  renderClassComponentWithRuntime,
58
+ type ClassComponentInstance,
42
59
  } from "./class-component.js";
43
- import { areMemoPropsEqual, getPendingProps } from "./prop-comparison.js";
60
+ import { areMemoPropsEqual, getPendingProps, shallowEqual } from "./prop-comparison.js";
44
61
  import {
45
62
  reportElementTextMismatch,
46
63
  reportExtraHydrationNodes,
@@ -58,16 +75,30 @@ interface MemoFiberState {
58
75
  instanceKeys: string[];
59
76
  }
60
77
 
78
+ interface FunctionFiberState {
79
+ element: ReactCompatElement;
80
+ props: Record<string, unknown>;
81
+ instanceKeys: string[];
82
+ hasContextDependencies: boolean;
83
+ }
84
+
61
85
  interface SuspenseFiberState {
62
86
  didSuspend: boolean;
63
87
  }
64
88
 
89
+ const committedPortalContainers = new Set<Element>();
90
+
65
91
  interface FiberHydrationOptions extends RenderOptions {
66
92
  previousNodes?: readonly Node[];
67
93
  resumeId?: string;
68
94
  consumeResumeMarkers?: boolean;
95
+ namespace?: HostNamespace;
96
+ documentRef?: Document;
69
97
  }
70
98
 
99
+ const SKIP_COMMIT_PATH = "\0";
100
+ const hasOwnProperty = Object.prototype.hasOwnProperty;
101
+
71
102
  interface FiberReconcileResult {
72
103
  fiber: Fiber | undefined;
73
104
  consumed: number;
@@ -166,16 +197,18 @@ export function renderHostFiberRoot(
166
197
  options: FiberHydrationOptions = {},
167
198
  ): Fiber {
168
199
  const workInProgress = createWorkInProgress(root.current, { children: element });
200
+ const rootDocument = root.container.ownerDocument;
169
201
  const result = reconcileHostChild(
170
202
  workInProgress,
171
203
  root.current.child,
172
204
  element,
173
205
  runtime,
174
206
  options.previousNodes === undefined ? "0" : "",
175
- options,
207
+ { ...options, documentRef: options.documentRef ?? rootDocument },
176
208
  );
177
209
  workInProgress.child = result.fiber;
178
210
  workInProgress.memoizedProps = { children: element };
211
+ root.refCleanupKnown = true;
179
212
  return workInProgress;
180
213
  }
181
214
 
@@ -204,8 +237,29 @@ export function commitHostFiberRoot(
204
237
  finishedWork: Fiber,
205
238
  options: RenderOptions = {},
206
239
  ): void {
207
- const nodes = commitHostChildren(finishedWork.child, root.container, root.container, "0", options);
208
- syncChildNodes(root.container, nodes);
240
+ runWithHostCommit(() => {
241
+ try {
242
+ committedPortalContainers.clear();
243
+ const commitPath = getRootCommitPath(options);
244
+ if (!hasChildListMutation(finishedWork)) {
245
+ commitHostDirtyChildren(finishedWork.child, root.container, root.container, commitPath, options);
246
+ return;
247
+ }
248
+
249
+ if (
250
+ !finishedWork.childListChanged &&
251
+ finishedWork.subtreeChildListChanged &&
252
+ commitHostKeyedChildListMutation(finishedWork.child, root.container, root.container, commitPath, options)
253
+ ) {
254
+ return;
255
+ }
256
+
257
+ const nodes = commitHostChildren(finishedWork.child, root.container, root.container, commitPath, options);
258
+ syncChildNodes(root.container, nodes);
259
+ } finally {
260
+ committedPortalContainers.clear();
261
+ }
262
+ });
209
263
  }
210
264
 
211
265
  export function commitHydratingHostFiberRoot(
@@ -214,9 +268,16 @@ export function commitHydratingHostFiberRoot(
214
268
  scope: HydrationScope,
215
269
  options: FiberHydrationOptions = {},
216
270
  ): void {
217
- const eventRoot = root.container;
218
- const nodes = commitHostChildren(finishedWork.child, scope.parent, eventRoot, "", options);
219
- syncScopedChildNodes(scope.parent, scope.before, scope.after, nodes);
271
+ runWithHostCommit(() => {
272
+ try {
273
+ committedPortalContainers.clear();
274
+ const eventRoot = root.container;
275
+ const nodes = commitHostChildren(finishedWork.child, scope.parent, eventRoot, "", options);
276
+ syncScopedChildNodes(scope.parent, scope.before, scope.after, nodes);
277
+ } finally {
278
+ committedPortalContainers.clear();
279
+ }
280
+ });
220
281
 
221
282
  if (options.consumeResumeMarkers === true) {
222
283
  scope.before?.parentNode?.removeChild(scope.before);
@@ -232,17 +293,70 @@ function reconcileHostChild(
232
293
  path: string,
233
294
  options: FiberHydrationOptions = {},
234
295
  ): FiberReconcileResult {
235
- const children = normalizeChildren(node);
236
- const existingByKey = collectExistingKeyedFibers(currentFirstChild);
296
+ resetFiberRefSubtree(parent);
297
+ parent.subtreeFlags = NoFlags;
298
+ parent.childListChanged = false;
299
+ parent.subtreeChildListChanged = false;
300
+
301
+ if (node === null || node === undefined || typeof node === "boolean") {
302
+ parent.childListChanged = currentFirstChild !== undefined;
303
+ return { fiber: undefined, consumed: 0 };
304
+ }
305
+
306
+ const children = Array.isArray(node) ? node : undefined;
307
+ const rowResult =
308
+ children === undefined
309
+ ? undefined
310
+ : reconcileKeyedRowHostChildren(parent, currentFirstChild, children, options);
311
+ if (rowResult !== undefined) {
312
+ return rowResult;
313
+ }
314
+
315
+ const childCount = children === undefined ? 1 : children.length;
316
+ const hasKeyedChildren = children !== undefined && hasKeyedChild(children);
317
+ let existingByKey: Map<string, Fiber> | undefined;
318
+ let currentKeyed: Fiber | undefined = currentFirstChild;
237
319
  let currentUnkeyed = currentFirstChild;
238
320
  let first: Fiber | undefined;
239
321
  let previous: Fiber | undefined;
240
322
  let consumed = 0;
323
+ let skipRemainingKeyedLookup = false;
241
324
 
242
- children.forEach((child, index) => {
325
+ for (let index = 0; index < childCount; index += 1) {
326
+ const child = children === undefined ? node : children[index];
243
327
  const key = getNodeKey(child);
244
- const matchedCurrent =
245
- key === undefined ? currentUnkeyed : existingByKey.get(key);
328
+ let matchedCurrent: Fiber | undefined;
329
+
330
+ if (key === undefined) {
331
+ matchedCurrent = currentUnkeyed;
332
+ } else if (skipRemainingKeyedLookup) {
333
+ matchedCurrent = undefined;
334
+ } else if (existingByKey !== undefined) {
335
+ matchedCurrent = existingByKey.get(key);
336
+ } else if (currentKeyed?.key === key) {
337
+ matchedCurrent = currentKeyed;
338
+ currentKeyed = currentKeyed.sibling;
339
+ } else if (
340
+ children !== undefined &&
341
+ currentKeyed?.sibling?.key === key &&
342
+ canSkipSingleDeletedKeyedFiber(children, index, currentKeyed.sibling)
343
+ ) {
344
+ matchedCurrent = currentKeyed.sibling;
345
+ currentKeyed = currentKeyed.sibling.sibling;
346
+ } else {
347
+ if (
348
+ children !== undefined &&
349
+ hasKeyedChildren &&
350
+ canSkipRemainingKeyedLookup(currentKeyed, children, index)
351
+ ) {
352
+ skipRemainingKeyedLookup = true;
353
+ currentKeyed = undefined;
354
+ } else if (hasKeyedChildren) {
355
+ existingByKey = collectExistingKeyedFibers(currentKeyed);
356
+ matchedCurrent = existingByKey.get(key);
357
+ }
358
+ }
359
+
246
360
  const previousNodes =
247
361
  options.previousNodes === undefined
248
362
  ? undefined
@@ -253,13 +367,13 @@ function reconcileHostChild(
253
367
  child,
254
368
  key,
255
369
  runtime,
256
- joinPath(path, getNodePathSegment(child, index)),
370
+ getReconcileChildPath(path, child, index, options),
257
371
  previousNodes === undefined ? options : { ...options, previousNodes },
258
372
  );
259
373
  const fiber = result.fiber;
260
374
 
261
375
  if (fiber === undefined) {
262
- return;
376
+ continue;
263
377
  }
264
378
 
265
379
  if (key === undefined) {
@@ -275,7 +389,7 @@ function reconcileHostChild(
275
389
 
276
390
  fiber.return = parent;
277
391
  fiber.sibling = undefined;
278
- fiber.pendingProps = getPendingProps(child);
392
+ bubbleHostChild(parent, fiber);
279
393
  if (
280
394
  fiber.tag !== "memo" &&
281
395
  fiber.tag !== "function-component" &&
@@ -288,11 +402,479 @@ function reconcileHostChild(
288
402
  fiber.memoizedState = index;
289
403
  }
290
404
  previous = fiber;
291
- });
405
+ }
406
+
407
+ parent.childListChanged = childFiberListShapeChanged(currentFirstChild, first);
292
408
 
293
409
  return { fiber: first, consumed };
294
410
  }
295
411
 
412
+ function reconcileKeyedRowHostChildren(
413
+ parent: Fiber,
414
+ currentFirstChild: Fiber | undefined,
415
+ children: readonly ReactCompatNode[],
416
+ options: FiberHydrationOptions,
417
+ ): FiberReconcileResult | undefined {
418
+ if (
419
+ children.length === 0 ||
420
+ currentFirstChild === undefined ||
421
+ options.previousNodes !== undefined ||
422
+ !shouldUseDirectHostTextChild()
423
+ ) {
424
+ return undefined;
425
+ }
426
+
427
+ let currentKeyed: Fiber | undefined = currentFirstChild;
428
+ let first: Fiber | undefined;
429
+ let previous: Fiber | undefined;
430
+ let listShapeChanged = currentFirstChild === undefined;
431
+ let skipRemainingKeyedLookup = false;
432
+ let subtreeFlags = NoFlags;
433
+ let subtreeChildListChanged = false;
434
+ let hasRefSubtree = false;
435
+ const currentSiblingCount = countFiberSiblings(currentFirstChild);
436
+ const canReuseUnchangedRows =
437
+ currentSiblingCount === children.length ||
438
+ isKeyedAppendOnly(currentFirstChild, children, currentSiblingCount);
439
+ const row = createKeyedRowHostElementScratch();
440
+
441
+ for (let index = 0; index < children.length; index += 1) {
442
+ const child = children[index];
443
+
444
+ if (!readKeyedRowHostElement(child, row)) {
445
+ return undefined;
446
+ }
447
+
448
+ let matchedCurrent: Fiber | undefined;
449
+
450
+ if (skipRemainingKeyedLookup) {
451
+ matchedCurrent = undefined;
452
+ } else if (currentKeyed === undefined) {
453
+ listShapeChanged = true;
454
+ skipRemainingKeyedLookup = true;
455
+ matchedCurrent = undefined;
456
+ } else if (currentKeyed?.key === row.key) {
457
+ matchedCurrent = currentKeyed;
458
+ currentKeyed = currentKeyed.sibling;
459
+ } else if (
460
+ currentKeyed?.sibling?.key === row.key &&
461
+ canSkipSingleDeletedKeyedFiber(children, index, currentKeyed.sibling)
462
+ ) {
463
+ listShapeChanged = true;
464
+ matchedCurrent = currentKeyed.sibling;
465
+ currentKeyed = currentKeyed.sibling.sibling;
466
+ } else if (canSkipRemainingKeyedLookup(currentKeyed, children, index)) {
467
+ listShapeChanged = true;
468
+ skipRemainingKeyedLookup = true;
469
+ currentKeyed = undefined;
470
+ matchedCurrent = undefined;
471
+ } else {
472
+ return undefined;
473
+ }
474
+
475
+ const fiber =
476
+ matchedCurrent === undefined
477
+ ? createKeyedRowHostFiber(parent, undefined, row, options)
478
+ : (canReuseUnchangedRows ? getReusableKeyedRowHostFiber(matchedCurrent, row) : undefined) ??
479
+ createKeyedRowHostFiber(parent, matchedCurrent, row, options);
480
+
481
+ if (first === undefined) {
482
+ first = fiber;
483
+ } else if (previous !== undefined) {
484
+ previous.sibling = fiber;
485
+ }
486
+
487
+ fiber.return = parent;
488
+ fiber.sibling = undefined;
489
+ if (fiber.hasRefSubtree) {
490
+ hasRefSubtree = true;
491
+ }
492
+ subtreeFlags |= fiber.flags | fiber.subtreeFlags;
493
+ subtreeChildListChanged =
494
+ subtreeChildListChanged ||
495
+ fiber.childListChanged ||
496
+ fiber.subtreeChildListChanged;
497
+ if (fiber.memoizedState === undefined) {
498
+ fiber.memoizedState = index;
499
+ }
500
+ previous = fiber;
501
+ }
502
+
503
+ if (currentKeyed !== undefined) {
504
+ listShapeChanged = true;
505
+ }
506
+
507
+ parent.hasRefSubtree = hasRefSubtree;
508
+ parent.subtreeFlags = subtreeFlags;
509
+ parent.subtreeChildListChanged = subtreeChildListChanged;
510
+ parent.childListChanged = listShapeChanged;
511
+ return { fiber: first, consumed: 0 };
512
+ }
513
+
514
+ function countFiberSiblings(first: Fiber): number {
515
+ let count = 0;
516
+ let cursor: Fiber | undefined = first;
517
+
518
+ while (cursor !== undefined) {
519
+ count += 1;
520
+ cursor = cursor.sibling;
521
+ }
522
+
523
+ return count;
524
+ }
525
+
526
+ function isKeyedAppendOnly(
527
+ currentFirstChild: Fiber,
528
+ children: readonly ReactCompatNode[],
529
+ currentSiblingCount: number,
530
+ ): boolean {
531
+ if (children.length <= currentSiblingCount) {
532
+ return false;
533
+ }
534
+
535
+ let current: Fiber | undefined = currentFirstChild;
536
+
537
+ for (let index = 0; index < currentSiblingCount; index += 1) {
538
+ if (current === undefined || current.key !== getNodeKey(children[index])) {
539
+ return false;
540
+ }
541
+ current = current.sibling;
542
+ }
543
+
544
+ return current === undefined;
545
+ }
546
+
547
+ function getReusableKeyedRowHostFiber(
548
+ current: Fiber,
549
+ row: KeyedRowHostElement,
550
+ ): Fiber | undefined {
551
+ if (
552
+ current.tag !== "host-component" ||
553
+ current.type !== row.type ||
554
+ current.hydrateExisting ||
555
+ current.child !== undefined ||
556
+ !isHostElement(current.stateNode)
557
+ ) {
558
+ return undefined;
559
+ }
560
+
561
+ const previousProps = current.memoizedProps ?? current.pendingProps;
562
+
563
+ if (typeof previousProps !== "object" || previousProps === null) {
564
+ return undefined;
565
+ }
566
+
567
+ const previousRecord = previousProps as Record<string, unknown>;
568
+
569
+ if (
570
+ getHostOwnPropsMeta(previousRecord) !== row.meta ||
571
+ getDirectHostTextChild(previousRecord.children) !== row.text
572
+ ) {
573
+ return undefined;
574
+ }
575
+
576
+ current.pendingProps = row.element.props;
577
+ current.flags = NoFlags;
578
+ current.subtreeFlags = NoFlags;
579
+ current.childListChanged = false;
580
+ current.subtreeChildListChanged = false;
581
+ current.hostChildListChanged = false;
582
+ current.hasRefSubtree = false;
583
+ return current;
584
+ }
585
+
586
+ interface KeyedRowHostElement {
587
+ element: ReactCompatElement;
588
+ key: string;
589
+ type: string;
590
+ meta: number;
591
+ text: string;
592
+ }
593
+
594
+ function createKeyedRowHostElementScratch(): KeyedRowHostElement {
595
+ return {
596
+ element: undefined as unknown as ReactCompatElement,
597
+ key: "",
598
+ type: "",
599
+ meta: 0,
600
+ text: "",
601
+ };
602
+ }
603
+
604
+ function readKeyedRowHostElement(
605
+ node: ReactCompatNode,
606
+ row: KeyedRowHostElement,
607
+ ): boolean {
608
+ if (
609
+ !isReactCompatElement(node) ||
610
+ typeof node.type !== "string" ||
611
+ node.key === null ||
612
+ node.ref !== null
613
+ ) {
614
+ return false;
615
+ }
616
+
617
+ const props = node.props as Record<string, unknown>;
618
+ const meta = getHostOwnPropsMeta(props);
619
+ const text = meta === undefined ? undefined : getDirectHostTextChild(props.children);
620
+
621
+ if (meta === undefined || text === undefined) {
622
+ return false;
623
+ }
624
+
625
+ row.element = node;
626
+ row.key = node.key;
627
+ row.type = node.type;
628
+ row.meta = meta;
629
+ row.text = text;
630
+ return true;
631
+ }
632
+
633
+ function createKeyedRowHostFiber(
634
+ parent: Fiber,
635
+ current: Fiber | undefined,
636
+ row: KeyedRowHostElement,
637
+ options: FiberHydrationOptions,
638
+ ): Fiber {
639
+ const node = row.element;
640
+ const elementNamespace = namespaceForHostElement(options.namespace ?? "html", row.type);
641
+ const fiber =
642
+ current?.tag === "host-component" && current.type === row.type
643
+ ? createWorkInProgress(current, node.props)
644
+ : createFiber("host-component", node.props, row.key);
645
+
646
+ fiber.type = row.type;
647
+ fiber.stateNode =
648
+ current?.tag === "host-component" &&
649
+ current.type === row.type &&
650
+ isHostElement(current.stateNode) &&
651
+ hostElementMatches(current.stateNode, row.type, elementNamespace)
652
+ ? current.stateNode
653
+ : createHostElement(getDocumentRef(options), row.type, options.namespace ?? "html");
654
+ fiber.child = undefined;
655
+ fiber.pendingProps = node.props;
656
+ fiber.hostChildListChanged = false;
657
+ fiber.hasRefSubtree = false;
658
+
659
+ if (current === undefined || fiber.alternate !== current) {
660
+ fiber.flags |= Placement;
661
+ fiber.hostChildListChanged = true;
662
+ return fiber;
663
+ }
664
+
665
+ const previousProps = current.memoizedProps ?? current.pendingProps;
666
+ const previousMeta =
667
+ typeof previousProps === "object" && previousProps !== null
668
+ ? getHostOwnPropsMeta(previousProps as Record<string, unknown>)
669
+ : undefined;
670
+ const previousText = getDirectHostTextChild(hostFiberChildrenProp(previousProps));
671
+
672
+ if (previousMeta !== row.meta || previousText !== row.text) {
673
+ fiber.flags |= Update;
674
+ }
675
+
676
+ return fiber;
677
+ }
678
+
679
+ function canReuseStaticHostSubtree(fiber: Fiber | undefined): boolean {
680
+ let cursor = fiber;
681
+
682
+ while (cursor !== undefined) {
683
+ if (
684
+ cursor.tag !== "host-component" &&
685
+ cursor.tag !== "host-text" &&
686
+ cursor.tag !== "fragment" &&
687
+ cursor.tag !== "strict-mode"
688
+ ) {
689
+ return false;
690
+ }
691
+
692
+ if (cursor.child !== undefined && !canReuseStaticHostSubtree(cursor.child)) {
693
+ return false;
694
+ }
695
+
696
+ cursor = cursor.sibling;
697
+ }
698
+
699
+ return true;
700
+ }
701
+
702
+ function canSkipSingleDeletedKeyedFiber(
703
+ children: readonly ReactCompatNode[],
704
+ index: number,
705
+ matched: Fiber,
706
+ ): boolean {
707
+ const nextKey = index + 1 < children.length ? getNodeKey(children[index + 1]) : undefined;
708
+ const afterMatched = matched.sibling;
709
+
710
+ return nextKey === undefined ? afterMatched === undefined : afterMatched?.key === nextKey;
711
+ }
712
+
713
+ function canSkipRemainingKeyedLookup(
714
+ current: Fiber | undefined,
715
+ children: readonly ReactCompatNode[],
716
+ startIndex: number,
717
+ ): boolean {
718
+ const currentRange = readContiguousNumericFiberKeyRange(current);
719
+
720
+ if (currentRange === undefined) {
721
+ return false;
722
+ }
723
+
724
+ const nextRange = readContiguousNumericNodeKeyRange(children, startIndex);
725
+
726
+ if (nextRange === undefined) {
727
+ return false;
728
+ }
729
+
730
+ return (
731
+ currentRange.end < nextRange.start ||
732
+ nextRange.end < currentRange.start
733
+ );
734
+ }
735
+
736
+ function readContiguousNumericFiberKeyRange(
737
+ fiber: Fiber | undefined,
738
+ ): { start: number; end: number } | undefined {
739
+ let cursor = fiber;
740
+ let start: number | undefined;
741
+ let previous: number | undefined;
742
+
743
+ while (cursor !== undefined) {
744
+ const value = parseNumericKey(cursor.key);
745
+
746
+ if (value === undefined || (previous !== undefined && value !== previous + 1)) {
747
+ return undefined;
748
+ }
749
+
750
+ start ??= value;
751
+ previous = value;
752
+ cursor = cursor.sibling;
753
+ }
754
+
755
+ return start === undefined || previous === undefined
756
+ ? undefined
757
+ : { start, end: previous };
758
+ }
759
+
760
+ function readContiguousNumericNodeKeyRange(
761
+ children: readonly ReactCompatNode[],
762
+ startIndex: number,
763
+ ): { start: number; end: number } | undefined {
764
+ let start: number | undefined;
765
+ let previous: number | undefined;
766
+
767
+ for (let index = startIndex; index < children.length; index += 1) {
768
+ const value = parseNumericKey(getNodeKey(children[index]));
769
+
770
+ if (value === undefined || (previous !== undefined && value !== previous + 1)) {
771
+ return undefined;
772
+ }
773
+
774
+ start ??= value;
775
+ previous = value;
776
+ }
777
+
778
+ return start === undefined || previous === undefined
779
+ ? undefined
780
+ : { start, end: previous };
781
+ }
782
+
783
+ function parseNumericKey(key: string | undefined): number | undefined {
784
+ if (key === undefined || key.length === 0) {
785
+ return undefined;
786
+ }
787
+
788
+ const value = Number(key);
789
+
790
+ return Number.isSafeInteger(value) && String(value) === key ? value : undefined;
791
+ }
792
+
793
+ function childFiberListShapeChanged(
794
+ current: Fiber | undefined,
795
+ next: Fiber | undefined,
796
+ ): boolean {
797
+ let currentCursor = current;
798
+ let nextCursor = next;
799
+
800
+ while (currentCursor !== undefined && nextCursor !== undefined) {
801
+ const isSameSlot =
802
+ nextCursor === currentCursor ||
803
+ nextCursor.alternate === currentCursor;
804
+
805
+ if (
806
+ !isSameSlot ||
807
+ currentCursor.tag !== nextCursor.tag ||
808
+ currentCursor.type !== nextCursor.type ||
809
+ currentCursor.key !== nextCursor.key
810
+ ) {
811
+ return true;
812
+ }
813
+
814
+ currentCursor = currentCursor.sibling;
815
+ nextCursor = nextCursor.sibling;
816
+ }
817
+
818
+ return currentCursor !== undefined || nextCursor !== undefined;
819
+ }
820
+
821
+ function bubbleHostChild(parent: Fiber, child: Fiber): void {
822
+ if (child.hasRefSubtree) {
823
+ parent.hasRefSubtree = true;
824
+ }
825
+ parent.subtreeFlags |= child.flags | child.subtreeFlags;
826
+ parent.subtreeChildListChanged =
827
+ parent.subtreeChildListChanged ||
828
+ child.childListChanged ||
829
+ child.subtreeChildListChanged;
830
+ }
831
+
832
+ function resetFiberRefSubtree(fiber: Fiber): void {
833
+ fiber.hasRefSubtree = false;
834
+ }
835
+
836
+ function includeNodeRef(fiber: Fiber, node: ReactCompatNode): void {
837
+ fiber.hasRefSubtree =
838
+ fiber.hasRefSubtree ||
839
+ (isReactCompatElement(node) && node.ref !== null);
840
+ }
841
+
842
+ function markHostFiberEffects(
843
+ fiber: Fiber,
844
+ current: Fiber | undefined,
845
+ node: ReactCompatNode,
846
+ ): void {
847
+ if (current === undefined || fiber.alternate !== current) {
848
+ fiber.flags |= Placement;
849
+ fiber.hostChildListChanged = true;
850
+ return;
851
+ }
852
+
853
+ if (fiber.tag === "host-text") {
854
+ if (!Object.is(current.memoizedProps ?? current.pendingProps, fiber.pendingProps)) {
855
+ fiber.flags |= Update;
856
+ }
857
+ return;
858
+ }
859
+
860
+ if (fiber.tag !== "host-component") {
861
+ return;
862
+ }
863
+
864
+ const previousProps = current.memoizedProps ?? current.pendingProps;
865
+ const nextProps = fiber.pendingProps as Record<string, unknown>;
866
+
867
+ fiber.hostChildListChanged = hostChildListChanged(previousProps, nextProps);
868
+
869
+ if (!hostOwnPropsEqual(previousProps, nextProps) || hostDirectTextChildChanged(previousProps, nextProps)) {
870
+ fiber.flags |= Update;
871
+ }
872
+
873
+ if (isReactCompatElement(node) && node.ref !== null) {
874
+ fiber.flags |= Update;
875
+ }
876
+ }
877
+
296
878
  function createHostFiber(
297
879
  parent: Fiber,
298
880
  current: Fiber | undefined,
@@ -301,6 +883,48 @@ function createHostFiber(
301
883
  runtime: RootRuntime | undefined,
302
884
  path: string,
303
885
  options: FiberHydrationOptions = {},
886
+ ): FiberReconcileResult {
887
+ const result = createHostFiberImpl(parent, current, node, key, runtime, path, options);
888
+
889
+ if (result.fiber !== undefined) {
890
+ if (canFinalizeNewHostFiber(result.fiber, current, node, options)) {
891
+ result.fiber.flags |= Placement;
892
+ result.fiber.hostChildListChanged = true;
893
+ return result;
894
+ }
895
+
896
+ result.fiber.pendingProps = getPendingProps(node);
897
+ includeNodeRef(result.fiber, node);
898
+ markHostFiberEffects(result.fiber, current, node);
899
+ }
900
+
901
+ return result;
902
+ }
903
+
904
+ function canFinalizeNewHostFiber(
905
+ fiber: Fiber,
906
+ current: Fiber | undefined,
907
+ node: ReactCompatNode,
908
+ options: FiberHydrationOptions,
909
+ ): boolean {
910
+ return (
911
+ current === undefined &&
912
+ options.previousNodes === undefined &&
913
+ fiber.tag === "host-component" &&
914
+ isReactCompatElement(node) &&
915
+ node.ref === null &&
916
+ typeof node.type === "string"
917
+ );
918
+ }
919
+
920
+ function createHostFiberImpl(
921
+ parent: Fiber,
922
+ current: Fiber | undefined,
923
+ node: ReactCompatNode,
924
+ key: string | undefined,
925
+ runtime: RootRuntime | undefined,
926
+ path: string,
927
+ options: FiberHydrationOptions = {},
304
928
  ): FiberReconcileResult {
305
929
  if (node === null || node === undefined || typeof node === "boolean") {
306
930
  return { fiber: undefined, consumed: 0 };
@@ -322,7 +946,7 @@ function createHostFiber(
322
946
  ? existing
323
947
  : current?.tag === "host-text" && current.stateNode instanceof Text
324
948
  ? current.stateNode
325
- : document.createTextNode("");
949
+ : getDocumentRef(options).createTextNode("");
326
950
 
327
951
  if (existing instanceof Text && existing.data !== String(node)) {
328
952
  reportRecoverable(
@@ -522,7 +1146,7 @@ function createHostFiber(
522
1146
  const childResult = reconcileHostChild(
523
1147
  fiber,
524
1148
  current?.tag === "context-consumer" ? current.child : undefined,
525
- render(useContext(node.type.context)),
1149
+ render(useContext(consumerContext(node.type))),
526
1150
  runtime,
527
1151
  `${path}.consumer`,
528
1152
  options,
@@ -544,6 +1168,7 @@ function createHostFiber(
544
1168
  fiber.type = forwardRefType;
545
1169
  const rendered = renderWithRootRuntime(runtime, path, () =>
546
1170
  forwardRefType.render(node.props, node.ref),
1171
+ forwardRefType,
547
1172
  );
548
1173
  fiber.memoizedState = getDevToolsHookState(runtime, path);
549
1174
  const childOptions = withHydrationComponentStack(
@@ -581,7 +1206,8 @@ function createHostFiber(
581
1206
 
582
1207
  if (
583
1208
  previousMemoState !== undefined &&
584
- !hasDirtyInstance(runtime, previousMemoState.instanceKeys) &&
1209
+ !hasDirtyInstance(runtime, previousMemoState.instanceKeys, memoPath) &&
1210
+ !hasUnflushedMountEffectInstance(runtime, previousMemoState.instanceKeys) &&
585
1211
  areMemoPropsEqual(memoType, previousMemoState.props, node.props)
586
1212
  ) {
587
1213
  markActiveInstanceKeys(runtime, previousMemoState.instanceKeys);
@@ -604,6 +1230,11 @@ function createHostFiber(
604
1230
  options,
605
1231
  );
606
1232
  fiber.child = childResult.fiber;
1233
+ if (fiber.child !== undefined) {
1234
+ fiber.child.return = fiber;
1235
+ fiber.child.sibling = undefined;
1236
+ bubbleHostChild(fiber, fiber.child);
1237
+ }
607
1238
  fiber.memoizedState = {
608
1239
  props: { ...node.props },
609
1240
  instanceKeys: collectInstanceKeys(runtime, memoPath),
@@ -638,6 +1269,11 @@ function createHostFiber(
638
1269
  options,
639
1270
  );
640
1271
  fiber.child = childResult.fiber;
1272
+ if (fiber.child !== undefined) {
1273
+ fiber.child.return = fiber;
1274
+ fiber.child.sibling = undefined;
1275
+ bubbleHostChild(fiber, fiber.child);
1276
+ }
641
1277
  return { fiber, consumed: childResult.consumed };
642
1278
  }
643
1279
 
@@ -680,11 +1316,26 @@ function createHostFiber(
680
1316
  ? createWorkInProgress(current, node.props)
681
1317
  : createFiber("class-component", node.props, key);
682
1318
  fiber.type = classType;
1319
+ const previousClassChildKeys = collectInstanceKeys(runtime, `${path}.class`);
1320
+ const currentClassInstance =
1321
+ current?.tag === "class-component" && current.type === classType
1322
+ ? (current.stateNode as ClassComponentInstance)
1323
+ : undefined;
683
1324
  const rendered = renderClassComponentWithRuntime(
684
1325
  classType,
685
1326
  node.props,
686
1327
  runtime,
687
1328
  path,
1329
+ {
1330
+ ...(currentClassInstance === undefined
1331
+ ? {}
1332
+ : { currentInstance: currentClassInstance }),
1333
+ hasDirtyDescendant: hasDirtyInstance(
1334
+ runtime,
1335
+ previousClassChildKeys,
1336
+ `${path}.class`,
1337
+ ),
1338
+ },
688
1339
  );
689
1340
  applyRef(node.ref, rendered.kind === "skip" ? current?.stateNode : rendered.instance);
690
1341
 
@@ -738,13 +1389,44 @@ function createHostFiber(
738
1389
  return { fiber: undefined, consumed: 0 };
739
1390
  }
740
1391
 
1392
+ const previousFunctionState =
1393
+ current?.tag === "function-component"
1394
+ ? (current.stateNode as FunctionFiberState | undefined)
1395
+ : undefined;
741
1396
  const fiber =
742
1397
  current?.tag === "function-component" && current.type === node.type
743
1398
  ? createWorkInProgress(current, node.props)
744
1399
  : createFiber("function-component", node.props, key);
745
1400
  fiber.type = node.type;
1401
+
1402
+ const canReuseSameElement =
1403
+ previousFunctionState !== undefined &&
1404
+ previousFunctionState.hasContextDependencies !== true &&
1405
+ previousFunctionState.element === node;
1406
+ const canReuseExternalStoreSnapshot =
1407
+ previousFunctionState !== undefined &&
1408
+ runtime.externalStoreUpdate &&
1409
+ shallowEqual(previousFunctionState.props, node.props) &&
1410
+ !hasChangedContextDependency(runtime, previousFunctionState.instanceKeys);
1411
+
1412
+ if (
1413
+ runtime.strictReplayDepth === 0 &&
1414
+ previousFunctionState !== undefined &&
1415
+ (canReuseSameElement || canReuseExternalStoreSnapshot) &&
1416
+ !hasDirtyInstance(runtime, previousFunctionState.instanceKeys, path) &&
1417
+ !hasUnflushedMountEffectInstance(runtime, previousFunctionState.instanceKeys) &&
1418
+ !hasPendingAsyncChild(current?.child)
1419
+ ) {
1420
+ markActiveInstanceKeys(runtime, previousFunctionState.instanceKeys);
1421
+ fiber.child = current?.child;
1422
+ fiber.memoizedState = current?.memoizedState;
1423
+ fiber.stateNode = previousFunctionState;
1424
+ return { fiber, consumed: options.previousNodes?.length ?? 0 };
1425
+ }
1426
+
746
1427
  const rendered = renderWithRootRuntime(runtime, path, () =>
747
1428
  (node.type as (props: Record<string, unknown>) => ReactCompatNode)(node.props),
1429
+ node.type,
748
1430
  );
749
1431
  fiber.memoizedState = getDevToolsHookState(runtime, path);
750
1432
  const childOptions = withHydrationComponentStack(
@@ -760,6 +1442,13 @@ function createHostFiber(
760
1442
  childOptions,
761
1443
  );
762
1444
  fiber.child = childResult.fiber;
1445
+ const instanceKeys = collectInstanceKeys(runtime, path);
1446
+ fiber.stateNode = {
1447
+ element: node,
1448
+ props: { ...node.props },
1449
+ instanceKeys,
1450
+ hasContextDependencies: hasContextDependency(runtime, instanceKeys),
1451
+ } satisfies FunctionFiberState;
763
1452
  return { fiber, consumed: childResult.consumed };
764
1453
  }
765
1454
 
@@ -767,19 +1456,21 @@ function createHostFiber(
767
1456
  return { fiber: undefined, consumed: 0 };
768
1457
  }
769
1458
 
1459
+ const elementNamespace = namespaceForHostElement(options.namespace ?? "html", node.type);
1460
+ const childNamespace = namespaceForHostChildren(elementNamespace, node.type);
770
1461
  const fiber =
771
1462
  current?.tag === "host-component" && current.type === node.type
772
1463
  ? createWorkInProgress(current, node.props)
773
1464
  : createFiber("host-component", node.props, key);
774
1465
  const existing = options.previousNodes?.[0];
775
- const existingElement = existing instanceof HTMLElement ? existing : undefined;
1466
+ const existingElement = isHostElement(existing) ? existing : undefined;
776
1467
  const tagMatches =
777
1468
  existingElement !== undefined &&
778
- existingElement.tagName.toLowerCase() === node.type;
1469
+ hostElementMatches(existingElement, node.type, elementNamespace);
779
1470
 
780
1471
  if (existing === undefined && options.previousNodes !== undefined) {
781
1472
  reportMissingHydrationNode(options, path);
782
- } else if (existing !== undefined && !(existing instanceof HTMLElement)) {
1473
+ } else if (existing !== undefined && !isHostElement(existing)) {
783
1474
  reportHydrationNodeTypeMismatch(options, path, `<${node.type}>`, existing);
784
1475
  }
785
1476
 
@@ -801,23 +1492,52 @@ function createHostFiber(
801
1492
  ? existingElement
802
1493
  : current?.tag === "host-component" &&
803
1494
  current.type === node.type &&
804
- current.stateNode instanceof HTMLElement
1495
+ isHostElement(current.stateNode) &&
1496
+ hostElementMatches(current.stateNode, node.type, elementNamespace)
805
1497
  ? current.stateNode
806
- : document.createElement(node.type);
1498
+ : createHostElement(getDocumentRef(options), node.type, options.namespace ?? "html");
807
1499
  fiber.hydrateExisting = tagMatches && options.previousNodes !== undefined;
808
1500
  const previousChildNodes =
809
1501
  tagMatches && existingElement !== undefined
810
1502
  ? Array.from(existingElement.childNodes)
811
1503
  : undefined;
1504
+ const directTextChild =
1505
+ shouldUseDirectHostTextChild() && previousChildNodes === undefined
1506
+ ? getDirectHostTextChild(node.props.children)
1507
+ : undefined;
1508
+ if (
1509
+ previousChildNodes === undefined &&
1510
+ current?.tag === "host-component" &&
1511
+ current.type === node.type &&
1512
+ Object.is(hostFiberChildrenProp(current.memoizedProps), node.props.children) &&
1513
+ !hasDirtyInstance(runtime, [], `${path}.c`) &&
1514
+ canReuseStaticHostSubtree(current.child)
1515
+ ) {
1516
+ fiber.child = current.child;
1517
+ if (fiber.child !== undefined) {
1518
+ fiber.child.return = fiber;
1519
+ }
1520
+ parent.child ??= fiber;
1521
+ return { fiber, consumed: existing === undefined ? 0 : 1 };
1522
+ }
1523
+
1524
+ if (directTextChild !== undefined) {
1525
+ fiber.child = undefined;
1526
+ parent.child ??= fiber;
1527
+ return { fiber, consumed: existing === undefined ? 0 : 1 };
1528
+ }
1529
+
812
1530
  const childResult = reconcileHostChild(
813
1531
  fiber,
814
1532
  current?.tag === "host-component" ? current.child : undefined,
815
1533
  node.props.children as ReactCompatNode,
816
1534
  runtime,
817
1535
  `${path}.c`,
818
- previousChildNodes === undefined
819
- ? options
820
- : { ...options, previousNodes: previousChildNodes },
1536
+ {
1537
+ ...options,
1538
+ namespace: childNamespace,
1539
+ ...(previousChildNodes === undefined ? {} : { previousNodes: previousChildNodes }),
1540
+ },
821
1541
  );
822
1542
  fiber.child = childResult.fiber;
823
1543
  if (previousChildNodes !== undefined) {
@@ -845,18 +1565,402 @@ function commitHostChildren(
845
1565
  options: RenderOptions = {},
846
1566
  ): Node[] {
847
1567
  const nodes: Node[] = [];
848
- let cursor = fiber;
849
- let index = 0;
1568
+ let cursor = fiber;
1569
+ let index = 0;
1570
+
1571
+ while (cursor !== undefined) {
1572
+ for (const node of commitHostFiber(cursor, parent, eventRoot, joinCommitPath(path, String(index)), options)) {
1573
+ nodes.push(node);
1574
+ }
1575
+ cursor = cursor.sibling;
1576
+ index += 1;
1577
+ }
1578
+
1579
+ return nodes;
1580
+ }
1581
+
1582
+ function commitHostDirtyChildren(
1583
+ fiber: Fiber | undefined,
1584
+ parent: ParentNode,
1585
+ eventRoot: Element,
1586
+ path: string,
1587
+ options: RenderOptions = {},
1588
+ ): void {
1589
+ let cursor = fiber;
1590
+ let index = 0;
1591
+
1592
+ while (cursor !== undefined) {
1593
+ if (hasHostCommitWork(cursor)) {
1594
+ commitHostDirtyFiber(cursor, parent, eventRoot, joinCommitPath(path, String(index)), options);
1595
+ }
1596
+ cursor = cursor.sibling;
1597
+ index += 1;
1598
+ }
1599
+ }
1600
+
1601
+ function commitHostDirtyFiber(
1602
+ fiber: Fiber,
1603
+ parent: ParentNode,
1604
+ eventRoot: Element,
1605
+ path: string,
1606
+ options: RenderOptions = {},
1607
+ ): void {
1608
+ if (fiber.tag === "host-text") {
1609
+ commitHostFiber(fiber, parent, eventRoot, path, options);
1610
+ return;
1611
+ }
1612
+
1613
+ if (fiber.tag === "host-component") {
1614
+ const element = fiber.stateNode;
1615
+
1616
+ if (!isHostElement(element)) {
1617
+ finishCommittedFiber(fiber);
1618
+ return;
1619
+ }
1620
+
1621
+ const props = fiber.pendingProps as Record<string, unknown>;
1622
+ const propsAreUnchanged =
1623
+ fiber.hydrateExisting !== true &&
1624
+ hostPropsEqual(fiber.memoizedProps, props);
1625
+ const propsAreChildrenOnly =
1626
+ fiber.hydrateExisting !== true &&
1627
+ hostPropsAreChildrenOnly(fiber.memoizedProps) &&
1628
+ hostPropsAreChildrenOnly(props);
1629
+ const textOnlyRowUpdate =
1630
+ fiber.hydrateExisting !== true &&
1631
+ isRowTextOnlyUpdate(fiber.memoizedProps, props);
1632
+
1633
+ if (!propsAreUnchanged && !propsAreChildrenOnly && !textOnlyRowUpdate) {
1634
+ applyProps(element, props, path, {
1635
+ ...options,
1636
+ eventRoot,
1637
+ preserveHydrationAttributes: fiber.hydrateExisting,
1638
+ });
1639
+ applyRef(props.ref, element);
1640
+ }
1641
+
1642
+ const directTextChild =
1643
+ fiber.child === undefined && fiber.hydrateExisting !== true
1644
+ ? getDirectHostTextChild(props.children)
1645
+ : undefined;
1646
+
1647
+ if (directTextChild !== undefined) {
1648
+ syncDirectHostTextChild(element, directTextChild);
1649
+ } else if (fiber.subtreeFlags !== NoFlags) {
1650
+ commitHostDirtyChildren(fiber.child, element, eventRoot, `${path}.c`, options);
1651
+ }
1652
+
1653
+ if (!propsAreUnchanged && !propsAreChildrenOnly && !textOnlyRowUpdate) {
1654
+ applyPostChildFormProps(element, props);
1655
+ }
1656
+ fiber.memoizedProps = props;
1657
+ finishCommittedFiber(fiber);
1658
+ return;
1659
+ }
1660
+
1661
+ if (fiber.tag === "portal") {
1662
+ const container = fiber.stateNode;
1663
+
1664
+ if (container instanceof Element) {
1665
+ setLogicalEventParent(container, parent);
1666
+ commitHostDirtyChildren(fiber.child, container, container, `${path}.portal`, options);
1667
+ }
1668
+ fiber.memoizedProps = fiber.pendingProps;
1669
+ finishCommittedFiber(fiber);
1670
+ return;
1671
+ }
1672
+
1673
+ if (fiber.subtreeFlags !== NoFlags) {
1674
+ commitHostDirtyChildren(fiber.child, parent, eventRoot, path, options);
1675
+ }
1676
+ fiber.memoizedProps = fiber.pendingProps;
1677
+ finishCommittedFiber(fiber);
1678
+ }
1679
+
1680
+ function hasHostCommitWork(fiber: Fiber): boolean {
1681
+ return (
1682
+ fiber.flags !== NoFlags ||
1683
+ fiber.subtreeFlags !== NoFlags ||
1684
+ fiber.hostChildListChanged ||
1685
+ fiber.childListChanged ||
1686
+ fiber.subtreeChildListChanged ||
1687
+ fiber.hydrateExisting
1688
+ );
1689
+ }
1690
+
1691
+ function commitHostKeyedChildListMutation(
1692
+ fiber: Fiber | undefined,
1693
+ parent: ParentNode,
1694
+ eventRoot: Element,
1695
+ path: string,
1696
+ options: RenderOptions = {},
1697
+ ): boolean {
1698
+ let cursor = fiber;
1699
+ let index = 0;
1700
+ let committed = false;
1701
+
1702
+ while (cursor !== undefined) {
1703
+ if (!hasHostCommitWork(cursor)) {
1704
+ cursor = cursor.sibling;
1705
+ index += 1;
1706
+ continue;
1707
+ }
1708
+
1709
+ const childPath = joinCommitPath(path, String(index));
1710
+ const didCommit = commitHostKeyedChildListMutationFiber(
1711
+ cursor,
1712
+ parent,
1713
+ eventRoot,
1714
+ childPath,
1715
+ options,
1716
+ );
1717
+
1718
+ if (!didCommit) {
1719
+ return false;
1720
+ }
1721
+
1722
+ committed = true;
1723
+ cursor = cursor.sibling;
1724
+ index += 1;
1725
+ }
1726
+
1727
+ return committed;
1728
+ }
1729
+
1730
+ function commitHostKeyedChildListMutationFiber(
1731
+ fiber: Fiber,
1732
+ parent: ParentNode,
1733
+ eventRoot: Element,
1734
+ path: string,
1735
+ options: RenderOptions = {},
1736
+ ): boolean {
1737
+ if (fiber.childListChanged) {
1738
+ const mutationParent =
1739
+ fiber.tag === "host-component" && isHostElement(fiber.stateNode)
1740
+ ? fiber.stateNode
1741
+ : parent;
1742
+
1743
+ if (fiber.tag === "host-component" && !isHostElement(fiber.stateNode)) {
1744
+ return false;
1745
+ }
1746
+
1747
+ if (commitHostAppendSuffix(fiber, mutationParent, eventRoot, path, options)) {
1748
+ finishHostPassthroughFiber(fiber);
1749
+ return true;
1750
+ }
1751
+
1752
+ if (commitHostSingleRemoval(fiber, mutationParent)) {
1753
+ finishHostPassthroughFiber(fiber);
1754
+ return true;
1755
+ }
1756
+
1757
+ return false;
1758
+ }
1759
+
1760
+ if (fiber.subtreeChildListChanged) {
1761
+ if (fiber.tag === "host-component") {
1762
+ const element = fiber.stateNode;
1763
+
1764
+ if (!isHostElement(element)) {
1765
+ return false;
1766
+ }
1767
+
1768
+ if (!commitHostKeyedChildListMutation(fiber.child, element, eventRoot, `${path}.c`, options)) {
1769
+ return false;
1770
+ }
1771
+ finishHostPassthroughFiber(fiber);
1772
+ return true;
1773
+ }
1774
+
1775
+ if (!commitHostKeyedChildListMutation(fiber.child, parent, eventRoot, path, options)) {
1776
+ return false;
1777
+ }
1778
+ finishHostPassthroughFiber(fiber);
1779
+ return true;
1780
+ }
1781
+
1782
+ commitHostDirtyFiber(fiber, parent, eventRoot, path, options);
1783
+ return true;
1784
+ }
1785
+
1786
+ function commitHostAppendSuffix(
1787
+ fiber: Fiber,
1788
+ parent: ParentNode,
1789
+ eventRoot: Element,
1790
+ path: string,
1791
+ options: RenderOptions,
1792
+ ): boolean {
1793
+ const append = getPlacementAppendSuffix(fiber.child) ?? getAppendSuffix(fiber.alternate?.child, fiber.child);
1794
+
1795
+ if (append === undefined) {
1796
+ return false;
1797
+ }
1798
+
1799
+ let cursor: Fiber | undefined = append.fiber;
1800
+ let index = append.index;
1801
+
1802
+ while (cursor !== undefined) {
1803
+ for (const node of commitHostFiber(cursor, parent, eventRoot, joinCommitPath(path, String(index)), options)) {
1804
+ parent.appendChild(node);
1805
+ }
1806
+ cursor = cursor.sibling;
1807
+ index += 1;
1808
+ }
1809
+
1810
+ return true;
1811
+ }
1812
+
1813
+ function getPlacementAppendSuffix(next: Fiber | undefined): { fiber: Fiber; index: number } | undefined {
1814
+ let nextCursor = next;
1815
+ let index = 0;
1816
+
1817
+ while (nextCursor !== undefined) {
1818
+ if ((nextCursor.flags & Placement) !== NoFlags) {
1819
+ if (index === 0) {
1820
+ return undefined;
1821
+ }
1822
+
1823
+ let appendCursor: Fiber | undefined = nextCursor;
1824
+
1825
+ while (appendCursor !== undefined) {
1826
+ if ((appendCursor.flags & Placement) === NoFlags) {
1827
+ return undefined;
1828
+ }
1829
+ appendCursor = appendCursor.sibling;
1830
+ }
1831
+
1832
+ return { fiber: nextCursor, index };
1833
+ }
1834
+
1835
+ if (hasHostCommitWork(nextCursor)) {
1836
+ return undefined;
1837
+ }
1838
+
1839
+ nextCursor = nextCursor.sibling;
1840
+ index += 1;
1841
+ }
1842
+
1843
+ return undefined;
1844
+ }
1845
+
1846
+ function commitHostSingleRemoval(fiber: Fiber, parent: ParentNode): boolean {
1847
+ const removed = getSingleRemovedFiber(fiber.alternate?.child, fiber.child);
1848
+
1849
+ if (removed === undefined) {
1850
+ return false;
1851
+ }
1852
+
1853
+ let removedAny = false;
1854
+
1855
+ for (const node of collectCommittedHostNodes(removed)) {
1856
+ if (node.parentNode !== parent) {
1857
+ return false;
1858
+ }
1859
+
1860
+ parent.removeChild(node);
1861
+ removedAny = true;
1862
+ }
1863
+
1864
+ return removedAny;
1865
+ }
1866
+
1867
+ function getAppendSuffix(
1868
+ current: Fiber | undefined,
1869
+ next: Fiber | undefined,
1870
+ ): { fiber: Fiber; index: number } | undefined {
1871
+ let currentCursor = current;
1872
+ let nextCursor = next;
1873
+ let index = 0;
1874
+
1875
+ while (currentCursor !== undefined && nextCursor !== undefined) {
1876
+ if (!isSameFiberSlot(currentCursor, nextCursor) || hasHostCommitWork(nextCursor)) {
1877
+ return undefined;
1878
+ }
1879
+
1880
+ currentCursor = currentCursor.sibling;
1881
+ nextCursor = nextCursor.sibling;
1882
+ index += 1;
1883
+ }
1884
+
1885
+ if (currentCursor !== undefined || nextCursor === undefined) {
1886
+ return undefined;
1887
+ }
1888
+
1889
+ return { fiber: nextCursor, index };
1890
+ }
1891
+
1892
+ function getSingleRemovedFiber(
1893
+ current: Fiber | undefined,
1894
+ next: Fiber | undefined,
1895
+ ): Fiber | undefined {
1896
+ let currentCursor = current;
1897
+ let nextCursor = next;
1898
+
1899
+ while (currentCursor !== undefined && nextCursor !== undefined) {
1900
+ if (!isSameFiberSlot(currentCursor, nextCursor)) {
1901
+ break;
1902
+ }
1903
+
1904
+ if (hasHostCommitWork(nextCursor)) {
1905
+ return undefined;
1906
+ }
1907
+
1908
+ currentCursor = currentCursor.sibling;
1909
+ nextCursor = nextCursor.sibling;
1910
+ }
1911
+
1912
+ if (currentCursor === undefined) {
1913
+ return undefined;
1914
+ }
1915
+
1916
+ const removed = currentCursor;
1917
+ currentCursor = currentCursor.sibling;
1918
+
1919
+ while (currentCursor !== undefined && nextCursor !== undefined) {
1920
+ if (!isSameFiberSlot(currentCursor, nextCursor) || hasHostCommitWork(nextCursor)) {
1921
+ return undefined;
1922
+ }
1923
+
1924
+ currentCursor = currentCursor.sibling;
1925
+ nextCursor = nextCursor.sibling;
1926
+ }
1927
+
1928
+ return currentCursor === undefined && nextCursor === undefined ? removed : undefined;
1929
+ }
1930
+
1931
+ function isSameFiberSlot(current: Fiber, next: Fiber): boolean {
1932
+ return (
1933
+ (next === current || next.alternate === current) &&
1934
+ current.tag === next.tag &&
1935
+ current.type === next.type &&
1936
+ current.key === next.key
1937
+ );
1938
+ }
1939
+
1940
+ function collectCommittedHostNodes(fiber: Fiber): Node[] {
1941
+ if (
1942
+ (fiber.tag === "host-component" || fiber.tag === "host-text") &&
1943
+ fiber.stateNode instanceof Node
1944
+ ) {
1945
+ return [fiber.stateNode];
1946
+ }
1947
+
1948
+ const nodes: Node[] = [];
1949
+ let child = fiber.child;
850
1950
 
851
- while (cursor !== undefined) {
852
- nodes.push(...commitHostFiber(cursor, parent, eventRoot, joinPath(path, String(index)), options));
853
- cursor = cursor.sibling;
854
- index += 1;
1951
+ while (child !== undefined) {
1952
+ nodes.push(...collectCommittedHostNodes(child));
1953
+ child = child.sibling;
855
1954
  }
856
1955
 
857
1956
  return nodes;
858
1957
  }
859
1958
 
1959
+ function finishHostPassthroughFiber(fiber: Fiber): void {
1960
+ fiber.memoizedProps = fiber.pendingProps;
1961
+ finishCommittedFiber(fiber);
1962
+ }
1963
+
860
1964
  function commitHostFiber(
861
1965
  fiber: Fiber,
862
1966
  parent: ParentNode,
@@ -868,92 +1972,171 @@ function commitHostFiber(
868
1972
  const text = fiber.stateNode;
869
1973
 
870
1974
  if (!(text instanceof Text)) {
1975
+ finishCommittedFiber(fiber);
871
1976
  return [];
872
1977
  }
873
1978
 
874
- text.data = String(fiber.pendingProps);
1979
+ const nextText = String(fiber.pendingProps);
1980
+ if (text.data !== nextText) {
1981
+ text.data = nextText;
1982
+ }
875
1983
  fiber.memoizedProps = fiber.pendingProps;
1984
+ finishCommittedFiber(fiber);
876
1985
  return [text];
877
1986
  }
878
1987
 
879
1988
  if (fiber.tag === "host-component") {
880
1989
  const element = fiber.stateNode;
881
1990
 
882
- if (!(element instanceof HTMLElement)) {
1991
+ if (!isHostElement(element)) {
1992
+ finishCommittedFiber(fiber);
883
1993
  return [];
884
1994
  }
885
1995
 
886
- applyProps(element, fiber.pendingProps as Record<string, unknown>, path, {
887
- ...options,
888
- eventRoot,
889
- preserveHydrationAttributes: fiber.hydrateExisting,
890
- });
891
- applyRef((fiber.pendingProps as { ref?: unknown }).ref, element);
892
- const childNodes = commitHostChildren(fiber.child, element, eventRoot, `${path}.c`, options);
893
- syncChildNodes(element, childNodes);
894
- applyPostChildFormProps(element, fiber.pendingProps as Record<string, unknown>);
895
- fiber.memoizedProps = fiber.pendingProps;
1996
+ if (
1997
+ fiber.hydrateExisting !== true &&
1998
+ fiber.flags === NoFlags &&
1999
+ fiber.subtreeFlags === NoFlags &&
2000
+ fiber.hostChildListChanged !== true
2001
+ ) {
2002
+ fiber.memoizedProps = fiber.pendingProps;
2003
+ return [element];
2004
+ }
2005
+
2006
+ const props = fiber.pendingProps as Record<string, unknown>;
2007
+ const propsAreUnchanged =
2008
+ fiber.hydrateExisting !== true &&
2009
+ hostPropsEqual(fiber.memoizedProps, props);
2010
+ const propsAreChildrenOnly =
2011
+ fiber.hydrateExisting !== true &&
2012
+ hostPropsAreChildrenOnly(fiber.memoizedProps) &&
2013
+ hostPropsAreChildrenOnly(props);
2014
+ const textOnlyRowUpdate =
2015
+ fiber.hydrateExisting !== true &&
2016
+ isRowTextOnlyUpdate(fiber.memoizedProps, props);
2017
+
2018
+ if (!propsAreUnchanged && !propsAreChildrenOnly && !textOnlyRowUpdate) {
2019
+ applyProps(element, props, path, {
2020
+ ...options,
2021
+ eventRoot,
2022
+ preserveHydrationAttributes: fiber.hydrateExisting,
2023
+ });
2024
+ applyRef(props.ref, element);
2025
+ }
2026
+ const directTextChild =
2027
+ fiber.child === undefined && fiber.hydrateExisting !== true
2028
+ ? getDirectHostTextChild(props.children)
2029
+ : undefined;
2030
+
2031
+ if (directTextChild !== undefined) {
2032
+ syncDirectHostTextChild(element, directTextChild);
2033
+ } else if (
2034
+ fiber.hostChildListChanged ||
2035
+ fiber.childListChanged ||
2036
+ fiber.hydrateExisting === true ||
2037
+ (fiber.subtreeFlags & Placement) !== NoFlags
2038
+ ) {
2039
+ const childNodes = commitHostChildren(fiber.child, element, eventRoot, `${path}.c`, options);
2040
+ if (
2041
+ !(childNodes.length === 0 && committedPortalContainers.has(element)) &&
2042
+ !shouldPreserveContentEditableChildren(element, props, childNodes)
2043
+ ) {
2044
+ syncChildNodes(element, childNodes);
2045
+ }
2046
+ } else if (fiber.subtreeFlags !== NoFlags) {
2047
+ commitHostChildren(fiber.child, element, eventRoot, `${path}.c`, options);
2048
+ }
2049
+
2050
+ if (!propsAreUnchanged && !propsAreChildrenOnly && !textOnlyRowUpdate) {
2051
+ applyPostChildFormProps(element, props);
2052
+ }
2053
+ fiber.memoizedProps = props;
2054
+ finishCommittedFiber(fiber);
896
2055
  return [element];
897
2056
  }
898
2057
 
899
2058
  if (fiber.tag === "fragment") {
900
2059
  fiber.memoizedProps = fiber.pendingProps;
901
- return commitHostChildren(fiber.child, parent, eventRoot, `${path}.f`, options);
2060
+ const nodes = commitHostChildren(fiber.child, parent, eventRoot, `${path}.f`, options);
2061
+ finishCommittedFiber(fiber);
2062
+ return nodes;
902
2063
  }
903
2064
 
904
2065
  if (fiber.tag === "profiler") {
905
2066
  fiber.memoizedProps = fiber.pendingProps;
906
- return commitHostChildren(fiber.child, parent, eventRoot, `${path}.profiler`, options);
2067
+ const nodes = commitHostChildren(fiber.child, parent, eventRoot, `${path}.profiler`, options);
2068
+ finishCommittedFiber(fiber);
2069
+ return nodes;
907
2070
  }
908
2071
 
909
2072
  if (fiber.tag === "strict-mode") {
910
2073
  fiber.memoizedProps = fiber.pendingProps;
911
- return commitHostChildren(fiber.child, parent, eventRoot, `${path}.strict`, options);
2074
+ const nodes = commitHostChildren(fiber.child, parent, eventRoot, `${path}.strict`, options);
2075
+ finishCommittedFiber(fiber);
2076
+ return nodes;
912
2077
  }
913
2078
 
914
2079
  if (fiber.tag === "suspense") {
915
2080
  fiber.memoizedProps = fiber.pendingProps;
916
- return commitHostChildren(fiber.child, parent, eventRoot, `${path}.s`, options);
2081
+ const nodes = commitHostChildren(fiber.child, parent, eventRoot, `${path}.s`, options);
2082
+ finishCommittedFiber(fiber);
2083
+ return nodes;
917
2084
  }
918
2085
 
919
2086
  if (fiber.tag === "suspense-list") {
920
2087
  fiber.memoizedProps = fiber.pendingProps;
921
- return commitHostChildren(fiber.child, parent, eventRoot, `${path}.sl`, options);
2088
+ const nodes = commitHostChildren(fiber.child, parent, eventRoot, `${path}.sl`, options);
2089
+ finishCommittedFiber(fiber);
2090
+ return nodes;
922
2091
  }
923
2092
 
924
2093
  if (fiber.tag === "context-provider" || fiber.tag === "context-consumer") {
925
2094
  fiber.memoizedProps = fiber.pendingProps;
926
- return commitHostChildren(fiber.child, parent, eventRoot, `${path}.ctx`, options);
2095
+ const nodes = commitHostChildren(fiber.child, parent, eventRoot, `${path}.ctx`, options);
2096
+ finishCommittedFiber(fiber);
2097
+ return nodes;
927
2098
  }
928
2099
 
929
2100
  if (fiber.tag === "function-component") {
930
2101
  fiber.memoizedProps = fiber.pendingProps;
931
- return commitHostChildren(fiber.child, parent, eventRoot, `${path}.fc`, options);
2102
+ const nodes = commitHostChildren(fiber.child, parent, eventRoot, `${path}.fc`, options);
2103
+ finishCommittedFiber(fiber);
2104
+ return nodes;
932
2105
  }
933
2106
 
934
2107
  if (fiber.tag === "forward-ref") {
935
2108
  fiber.memoizedProps = fiber.pendingProps;
936
- return commitHostChildren(fiber.child, parent, eventRoot, `${path}.fr`, options);
2109
+ const nodes = commitHostChildren(fiber.child, parent, eventRoot, `${path}.fr`, options);
2110
+ finishCommittedFiber(fiber);
2111
+ return nodes;
937
2112
  }
938
2113
 
939
2114
  if (fiber.tag === "memo") {
940
2115
  fiber.memoizedProps = fiber.pendingProps;
941
- return commitHostChildren(fiber.child, parent, eventRoot, `${path}.memo`, options);
2116
+ const nodes = commitHostChildren(fiber.child, parent, eventRoot, `${path}.memo`, options);
2117
+ finishCommittedFiber(fiber);
2118
+ return nodes;
942
2119
  }
943
2120
 
944
2121
  if (fiber.tag === "lazy") {
945
2122
  fiber.memoizedProps = fiber.pendingProps;
946
- return commitHostChildren(fiber.child, parent, eventRoot, `${path}.lazy`, options);
2123
+ const nodes = commitHostChildren(fiber.child, parent, eventRoot, `${path}.lazy`, options);
2124
+ finishCommittedFiber(fiber);
2125
+ return nodes;
947
2126
  }
948
2127
 
949
2128
  if (fiber.tag === "error-boundary") {
950
2129
  fiber.memoizedProps = fiber.pendingProps;
951
- return commitHostChildren(fiber.child, parent, eventRoot, `${path}.eb`, options);
2130
+ const nodes = commitHostChildren(fiber.child, parent, eventRoot, `${path}.eb`, options);
2131
+ finishCommittedFiber(fiber);
2132
+ return nodes;
952
2133
  }
953
2134
 
954
2135
  if (fiber.tag === "class-component") {
955
2136
  fiber.memoizedProps = fiber.pendingProps;
956
- return commitHostChildren(fiber.child, parent, eventRoot, `${path}.class`, options);
2137
+ const nodes = commitHostChildren(fiber.child, parent, eventRoot, `${path}.class`, options);
2138
+ finishCommittedFiber(fiber);
2139
+ return nodes;
957
2140
  }
958
2141
 
959
2142
  if (fiber.tag === "portal") {
@@ -964,21 +2147,281 @@ function commitHostFiber(
964
2147
  }
965
2148
 
966
2149
  setLogicalEventParent(container, parent);
2150
+ committedPortalContainers.add(container);
2151
+ const portalEventRoot =
2152
+ eventRoot !== container && eventRoot.contains(container) ? eventRoot : container;
967
2153
  const childNodes = commitHostChildren(
968
2154
  fiber.child,
969
2155
  container,
970
- container,
2156
+ portalEventRoot,
971
2157
  `${path}.portal`,
972
2158
  options,
973
2159
  );
974
- syncChildNodes(container, childNodes);
2160
+ const previousNodes = Array.isArray(fiber.alternate?.memoizedState)
2161
+ ? fiber.alternate.memoizedState.filter((node): node is Node => node instanceof Node)
2162
+ : [];
2163
+ syncOwnedChildNodes(container, previousNodes, childNodes);
2164
+ fiber.memoizedState = childNodes;
975
2165
  fiber.memoizedProps = fiber.pendingProps;
2166
+ finishCommittedFiber(fiber);
976
2167
  return [];
977
2168
  }
978
2169
 
2170
+ finishCommittedFiber(fiber);
979
2171
  return [];
980
2172
  }
981
2173
 
2174
+ function finishCommittedFiber(fiber: Fiber): void {
2175
+ fiber.flags = NoFlags;
2176
+ fiber.subtreeFlags = NoFlags;
2177
+ fiber.childListChanged = false;
2178
+ fiber.subtreeChildListChanged = false;
2179
+ fiber.hostChildListChanged = false;
2180
+ }
2181
+
2182
+ function hasChildListMutation(fiber: Fiber): boolean {
2183
+ return fiber.childListChanged || fiber.subtreeChildListChanged;
2184
+ }
2185
+
2186
+ function hostPropsEqual(previous: unknown, next: Record<string, unknown>): boolean {
2187
+ if (previous === next) {
2188
+ return true;
2189
+ }
2190
+
2191
+ if (typeof previous !== "object" || previous === null) {
2192
+ return false;
2193
+ }
2194
+
2195
+ const previousProps = previous as Record<string, unknown>;
2196
+ let previousCount = 0;
2197
+ let nextCount = 0;
2198
+
2199
+ for (const key in previousProps) {
2200
+ if (!hasOwnProperty.call(previousProps, key)) {
2201
+ continue;
2202
+ }
2203
+ previousCount += 1;
2204
+ if (!hasOwnProperty.call(next, key)) {
2205
+ return false;
2206
+ }
2207
+
2208
+ if (!Object.is(previousProps[key], next[key])) {
2209
+ return false;
2210
+ }
2211
+ }
2212
+
2213
+ for (const key in next) {
2214
+ if (hasOwnProperty.call(next, key)) {
2215
+ nextCount += 1;
2216
+ }
2217
+ }
2218
+
2219
+ return previousCount === nextCount;
2220
+ }
2221
+
2222
+ function hostOwnPropsEqual(previous: unknown, next: Record<string, unknown>): boolean {
2223
+ if (previous === next) {
2224
+ return true;
2225
+ }
2226
+
2227
+ if (typeof previous !== "object" || previous === null) {
2228
+ return false;
2229
+ }
2230
+
2231
+ const previousProps = previous as Record<string, unknown>;
2232
+ const previousMeta = getHostOwnPropsMeta(previousProps);
2233
+ const nextMeta = getHostOwnPropsMeta(next);
2234
+
2235
+ if (previousMeta !== undefined && nextMeta !== undefined) {
2236
+ return previousMeta === nextMeta;
2237
+ }
2238
+
2239
+ let previousCount = 0;
2240
+ let nextCount = 0;
2241
+
2242
+ for (const key in previousProps) {
2243
+ if (!hasOwnProperty.call(previousProps, key) || key === "children") {
2244
+ continue;
2245
+ }
2246
+ previousCount += 1;
2247
+ if (!hasOwnProperty.call(next, key)) {
2248
+ return false;
2249
+ }
2250
+
2251
+ if (!Object.is(previousProps[key], next[key])) {
2252
+ return false;
2253
+ }
2254
+ }
2255
+
2256
+ for (const key in next) {
2257
+ if (hasOwnProperty.call(next, key) && key !== "children") {
2258
+ nextCount += 1;
2259
+ }
2260
+ }
2261
+
2262
+ return previousCount === nextCount;
2263
+ }
2264
+
2265
+ function getHostOwnPropsMeta(props: Record<string, unknown>): number | undefined {
2266
+ return (props as { [HOST_OWN_PROPS_META]?: number })[HOST_OWN_PROPS_META];
2267
+ }
2268
+
2269
+ function hostDirectTextChildChanged(previous: unknown, next: Record<string, unknown>): boolean {
2270
+ const previousText = getDirectHostTextChild(hostFiberChildrenProp(previous));
2271
+ const nextText = getDirectHostTextChild(next.children);
2272
+
2273
+ return (previousText !== undefined || nextText !== undefined) && previousText !== nextText;
2274
+ }
2275
+
2276
+ function hostChildListChanged(previous: unknown, next: Record<string, unknown>): boolean {
2277
+ const previousChildren = hostFiberChildrenProp(previous);
2278
+ const nextChildren = next.children;
2279
+
2280
+ if (Object.is(previousChildren, nextChildren)) {
2281
+ return false;
2282
+ }
2283
+
2284
+ if (
2285
+ getDirectHostTextChild(previousChildren) !== undefined ||
2286
+ getDirectHostTextChild(nextChildren) !== undefined
2287
+ ) {
2288
+ return false;
2289
+ }
2290
+
2291
+ if (sameSingleHostChild(previousChildren, nextChildren)) {
2292
+ return false;
2293
+ }
2294
+
2295
+ if (sameHostChildList(previousChildren, nextChildren)) {
2296
+ return false;
2297
+ }
2298
+
2299
+ return true;
2300
+ }
2301
+
2302
+ function sameSingleHostChild(previous: unknown, next: unknown): boolean {
2303
+ return (
2304
+ isReactCompatElement(previous) &&
2305
+ isReactCompatElement(next) &&
2306
+ previous.key === next.key &&
2307
+ previous.type === next.type
2308
+ );
2309
+ }
2310
+
2311
+ function sameHostChildList(previous: unknown, next: unknown): boolean {
2312
+ if (!Array.isArray(previous) || !Array.isArray(next) || previous.length !== next.length) {
2313
+ return false;
2314
+ }
2315
+
2316
+ for (let index = 0; index < previous.length; index += 1) {
2317
+ const previousChild = previous[index];
2318
+ const nextChild = next[index];
2319
+
2320
+ if (Object.is(previousChild, nextChild)) {
2321
+ continue;
2322
+ }
2323
+
2324
+ if (!sameSingleHostChild(previousChild, nextChild)) {
2325
+ return false;
2326
+ }
2327
+ }
2328
+
2329
+ return (
2330
+ previous.length > 0 ||
2331
+ (Array.isArray(previous) && Array.isArray(next))
2332
+ );
2333
+ }
2334
+
2335
+ function hostPropsAreChildrenOnly(props: unknown): boolean {
2336
+ if (typeof props !== "object" || props === null) {
2337
+ return false;
2338
+ }
2339
+
2340
+ for (const key in props) {
2341
+ if (
2342
+ Object.prototype.hasOwnProperty.call(props, key) &&
2343
+ key !== "children"
2344
+ ) {
2345
+ return false;
2346
+ }
2347
+ }
2348
+
2349
+ return true;
2350
+ }
2351
+
2352
+ function isRowTextOnlyUpdate(previous: unknown, next: Record<string, unknown>): boolean {
2353
+ if (typeof previous !== "object" || previous === null) {
2354
+ return false;
2355
+ }
2356
+
2357
+ const previousProps = previous as Record<string, unknown>;
2358
+ const previousMeta = getHostOwnPropsMeta(previousProps);
2359
+ const nextMeta = getHostOwnPropsMeta(next);
2360
+
2361
+ if (previousMeta === undefined || previousMeta !== nextMeta) {
2362
+ return false;
2363
+ }
2364
+
2365
+ const previousText = getDirectHostTextChild(previousProps.children);
2366
+ const nextText = getDirectHostTextChild(next.children);
2367
+
2368
+ return previousText !== undefined && nextText !== undefined && previousText !== nextText;
2369
+ }
2370
+
2371
+ function hostFiberChildrenProp(props: unknown): unknown {
2372
+ return typeof props === "object" && props !== null
2373
+ ? (props as { children?: unknown }).children
2374
+ : undefined;
2375
+ }
2376
+
2377
+ function getDirectHostTextChild(children: unknown): string | undefined {
2378
+ return typeof children === "string" || typeof children === "number"
2379
+ ? String(children)
2380
+ : undefined;
2381
+ }
2382
+
2383
+ function shouldUseDirectHostTextChild(): boolean {
2384
+ const globalProcess = (globalThis as { process?: { env?: Record<string, string | undefined> } })
2385
+ .process;
2386
+ return globalProcess?.env?.NODE_ENV === "production";
2387
+ }
2388
+
2389
+ function syncDirectHostTextChild(element: Element, text: string): void {
2390
+ const firstChild = element.firstChild;
2391
+
2392
+ if (firstChild instanceof Text && firstChild.nextSibling === null) {
2393
+ if (firstChild.data !== text) {
2394
+ firstChild.data = text;
2395
+ }
2396
+ return;
2397
+ }
2398
+
2399
+ element.textContent = text;
2400
+ }
2401
+
2402
+ function shouldPreserveContentEditableChildren(
2403
+ element: Element,
2404
+ props: Record<string, unknown>,
2405
+ childNodes: readonly Node[],
2406
+ ): boolean {
2407
+ void childNodes;
2408
+
2409
+ if (
2410
+ !element.hasAttribute("contenteditable") ||
2411
+ element.getAttribute("contenteditable") === "false"
2412
+ ) {
2413
+ return false;
2414
+ }
2415
+
2416
+ const children = props.children;
2417
+ return (
2418
+ children === undefined ||
2419
+ children === null ||
2420
+ children === false ||
2421
+ (Array.isArray(children) && children.length === 0)
2422
+ );
2423
+ }
2424
+
982
2425
  function createSuspenseFiber(
983
2426
  current: Fiber | undefined,
984
2427
  element: ReactCompatElement,
@@ -1078,25 +2521,8 @@ function createStrictModeFiber(
1078
2521
  ? createWorkInProgress(current, element.props)
1079
2522
  : createFiber("strict-mode", element.props, key);
1080
2523
  fiber.type = element.type;
1081
- const snapshot = takeRuntimeSnapshot(runtime);
1082
-
1083
- try {
1084
- createHostFiber(
1085
- fiber,
1086
- undefined,
1087
- element.props.children as ReactCompatNode,
1088
- undefined,
1089
- runtime,
1090
- `${path}.strict.preview`,
1091
- options.previousNodes === undefined
1092
- ? options
1093
- : { ...options, previousNodes: [] },
1094
- );
1095
- } finally {
1096
- restoreRuntimeSnapshot(runtime, snapshot);
1097
- }
1098
2524
 
1099
- const childResult = renderWithStrictMode(
2525
+ const { result: childResult, memoValues, memoValuesByHook } = renderWithStrictModeMemoCapture(
1100
2526
  runtime,
1101
2527
  () =>
1102
2528
  reconcileHostChild(
@@ -1109,6 +2535,29 @@ function createStrictModeFiber(
1109
2535
  ),
1110
2536
  );
1111
2537
  fiber.child = childResult.fiber;
2538
+
2539
+ const snapshot = takeRuntimeSnapshot(runtime);
2540
+ try {
2541
+ renderStrictModeReplay(
2542
+ runtime,
2543
+ memoValues,
2544
+ memoValuesByHook,
2545
+ () =>
2546
+ reconcileHostChild(
2547
+ fiber,
2548
+ childResult.fiber,
2549
+ element.props.children as ReactCompatNode,
2550
+ runtime,
2551
+ `${path}.strict`,
2552
+ options.previousNodes === undefined
2553
+ ? options
2554
+ : { ...options, previousNodes: [] },
2555
+ ),
2556
+ );
2557
+ } finally {
2558
+ restoreRuntimeSnapshot(runtime, snapshot);
2559
+ }
2560
+
1112
2561
  return { fiber, consumed: childResult.consumed };
1113
2562
  }
1114
2563
 
@@ -1220,6 +2669,7 @@ function reconcileSuspenseListForwards(
1220
2669
 
1221
2670
  fiber.return = parent;
1222
2671
  fiber.sibling = undefined;
2672
+ bubbleHostChild(parent, fiber);
1223
2673
 
1224
2674
  if (first === undefined) {
1225
2675
  first = fiber;
@@ -1272,6 +2722,7 @@ function reconcileSuspenseListBackwards(
1272
2722
 
1273
2723
  fiber.return = parent;
1274
2724
  fiber.sibling = undefined;
2725
+ bubbleHostChild(parent, fiber);
1275
2726
  fibers.unshift(fiber);
1276
2727
 
1277
2728
  if (isSuspendedSuspenseFiber(fiber)) {
@@ -1309,6 +2760,32 @@ function hasSuspendedChild(fiber: Fiber | undefined): boolean {
1309
2760
  return false;
1310
2761
  }
1311
2762
 
2763
+ function hasPendingAsyncChild(fiber: Fiber | undefined): boolean {
2764
+ let cursor = fiber;
2765
+
2766
+ while (cursor !== undefined) {
2767
+ if (isPendingLazyFiber(cursor) || isSuspendedSuspenseFiber(cursor)) {
2768
+ return true;
2769
+ }
2770
+
2771
+ if (cursor.child !== undefined && hasPendingAsyncChild(cursor.child)) {
2772
+ return true;
2773
+ }
2774
+
2775
+ cursor = cursor.sibling;
2776
+ }
2777
+
2778
+ return false;
2779
+ }
2780
+
2781
+ function isPendingLazyFiber(fiber: Fiber): boolean {
2782
+ if (fiber.tag !== "lazy" || !isLazyType(fiber.type)) {
2783
+ return false;
2784
+ }
2785
+
2786
+ return fiber.type.status !== "resolved" || fiber.child === undefined;
2787
+ }
2788
+
1312
2789
  function isSuspendedSuspenseFiber(fiber: Fiber): boolean {
1313
2790
  return (
1314
2791
  fiber.tag === "suspense" &&
@@ -1437,7 +2914,7 @@ function createPortalFiber(
1437
2914
  portal.children,
1438
2915
  runtime,
1439
2916
  `${path}.portal`,
1440
- options,
2917
+ { ...options, documentRef: portal.container.ownerDocument },
1441
2918
  );
1442
2919
  fiber.child = childResult.fiber;
1443
2920
  fiber.return = parent;
@@ -1452,6 +2929,10 @@ function normalizeChildren(node: ReactCompatNode): ReactCompatNode[] {
1452
2929
  return Array.isArray(node) ? node : [node];
1453
2930
  }
1454
2931
 
2932
+ function getDocumentRef(options: FiberHydrationOptions): Document {
2933
+ return options.documentRef ?? document;
2934
+ }
2935
+
1455
2936
  function collectExistingKeyedFibers(
1456
2937
  firstChild: Fiber | undefined,
1457
2938
  ): Map<string, Fiber> {
@@ -1473,11 +2954,65 @@ function getNodeKey(node: ReactCompatNode): string | undefined {
1473
2954
  return isReactCompatElement(node) && node.key !== null ? node.key : undefined;
1474
2955
  }
1475
2956
 
2957
+ function hasKeyedChild(children: readonly ReactCompatNode[]): boolean {
2958
+ for (const child of children) {
2959
+ if (getNodeKey(child) !== undefined) {
2960
+ return true;
2961
+ }
2962
+ }
2963
+
2964
+ return false;
2965
+ }
2966
+
1476
2967
  function getNodePathSegment(node: ReactCompatNode, index: number): string {
1477
2968
  const key = getNodeKey(node);
1478
2969
  return key === undefined ? String(index) : `k:${key}`;
1479
2970
  }
1480
2971
 
2972
+ function getReconcileChildPath(
2973
+ path: string,
2974
+ node: ReactCompatNode,
2975
+ index: number,
2976
+ options: FiberHydrationOptions,
2977
+ ): string {
2978
+ if (!shouldTrackReconcilePath(node, options)) {
2979
+ return "";
2980
+ }
2981
+
2982
+ return joinPath(path, getNodePathSegment(node, index));
2983
+ }
2984
+
2985
+ function shouldTrackReconcilePath(
2986
+ node: ReactCompatNode,
2987
+ options: FiberHydrationOptions,
2988
+ ): boolean {
2989
+ if (
2990
+ options.previousNodes !== undefined ||
2991
+ options.hydration?.onRecoverableError !== undefined ||
2992
+ options.resumeId !== undefined
2993
+ ) {
2994
+ return true;
2995
+ }
2996
+
2997
+ return !isHostElementWithDirectTextChild(node);
2998
+ }
2999
+
3000
+ function isHostElementWithDirectTextChild(node: ReactCompatNode): boolean {
3001
+ return (
3002
+ isReactCompatElement(node) &&
3003
+ typeof node.type === "string" &&
3004
+ getDirectHostTextChild(node.props.children) !== undefined
3005
+ );
3006
+ }
3007
+
3008
+ function getRootCommitPath(options: RenderOptions): string {
3009
+ return options.hydration?.onRecoverableError === undefined ? SKIP_COMMIT_PATH : "0";
3010
+ }
3011
+
3012
+ function joinCommitPath(path: string, segment: string): string {
3013
+ return path === SKIP_COMMIT_PATH ? "" : joinPath(path, segment);
3014
+ }
3015
+
1481
3016
  function getComponentName(component: Function): string {
1482
3017
  return component.name === "" ? "Anonymous" : component.name;
1483
3018
  }
@@ -1545,11 +3080,55 @@ function markActiveInstanceKeys(runtime: RootRuntime, keys: readonly string[]):
1545
3080
  }
1546
3081
  }
1547
3082
 
1548
- function hasDirtyInstance(runtime: RootRuntime, keys: readonly string[]): boolean {
1549
- return keys.some(
3083
+ function hasDirtyInstance(
3084
+ runtime: RootRuntime | undefined,
3085
+ keys: readonly string[],
3086
+ prefix?: string,
3087
+ ): boolean {
3088
+ if (runtime === undefined) {
3089
+ return false;
3090
+ }
3091
+
3092
+ if (hasDirtyClassUpdate(runtime, keys, prefix)) {
3093
+ return true;
3094
+ }
3095
+
3096
+ if (keys.some(
1550
3097
  (key) =>
1551
3098
  (runtime.instances.get(key) as { dirty?: boolean } | undefined)?.dirty === true,
1552
- );
3099
+ )) {
3100
+ return true;
3101
+ }
3102
+
3103
+ if (prefix === undefined) {
3104
+ return false;
3105
+ }
3106
+
3107
+ for (const [key, instance] of runtime.instances) {
3108
+ if (
3109
+ (key === prefix || key.startsWith(`${prefix}.`)) &&
3110
+ (instance as { dirty?: boolean }).dirty === true
3111
+ ) {
3112
+ return true;
3113
+ }
3114
+ }
3115
+
3116
+ return false;
3117
+ }
3118
+
3119
+ function hasUnflushedMountEffectInstance(runtime: RootRuntime, keys: readonly string[]): boolean {
3120
+ return keys.some((key) => {
3121
+ const instance = runtime.instances.get(key) as
3122
+ | { hooks?: readonly ({ kind?: string; mounted?: boolean; disposed?: boolean } | undefined)[] }
3123
+ | undefined;
3124
+
3125
+ return instance?.hooks?.some(
3126
+ (slot) =>
3127
+ slot?.kind === "effect" &&
3128
+ slot.disposed !== true &&
3129
+ slot.mounted !== true,
3130
+ ) === true;
3131
+ });
1553
3132
  }
1554
3133
 
1555
3134
  function applyRef(ref: unknown, node: unknown): void {