@reckona/mreact-compat 0.0.171 → 0.0.173

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 (58) hide show
  1. package/dist/devtools.js +2 -0
  2. package/dist/devtools.js.map +1 -1
  3. package/dist/dom-host-rules.d.ts.map +1 -1
  4. package/dist/dom-host-rules.js +4 -1
  5. package/dist/dom-host-rules.js.map +1 -1
  6. package/dist/dom-props.js +2 -1
  7. package/dist/dom-props.js.map +1 -1
  8. package/dist/element.d.ts +13 -1
  9. package/dist/element.d.ts.map +1 -1
  10. package/dist/element.js +30 -2
  11. package/dist/element.js.map +1 -1
  12. package/dist/fiber-commit.js +2 -1
  13. package/dist/fiber-commit.js.map +1 -1
  14. package/dist/fiber-host.d.ts +1 -1
  15. package/dist/fiber-host.d.ts.map +1 -1
  16. package/dist/fiber-host.js +1 -1
  17. package/dist/fiber-host.js.map +1 -1
  18. package/dist/fiber.d.ts +2 -1
  19. package/dist/fiber.d.ts.map +1 -1
  20. package/dist/fiber.js +2 -0
  21. package/dist/fiber.js.map +1 -1
  22. package/dist/hooks.d.ts +9 -0
  23. package/dist/hooks.d.ts.map +1 -1
  24. package/dist/hooks.js +127 -44
  25. package/dist/hooks.js.map +1 -1
  26. package/dist/host-reconciler.d.ts +2 -0
  27. package/dist/host-reconciler.d.ts.map +1 -1
  28. package/dist/host-reconciler.js +696 -55
  29. package/dist/host-reconciler.js.map +1 -1
  30. package/dist/jsx-dev-runtime.d.ts +3 -1
  31. package/dist/jsx-dev-runtime.d.ts.map +1 -1
  32. package/dist/jsx-dev-runtime.js +3 -1
  33. package/dist/jsx-dev-runtime.js.map +1 -1
  34. package/dist/jsx-runtime.d.ts +4 -2
  35. package/dist/jsx-runtime.d.ts.map +1 -1
  36. package/dist/jsx-runtime.js +9 -1
  37. package/dist/jsx-runtime.js.map +1 -1
  38. package/dist/reactive-prop-cell.d.ts +13 -0
  39. package/dist/reactive-prop-cell.d.ts.map +1 -0
  40. package/dist/reactive-prop-cell.js +36 -0
  41. package/dist/reactive-prop-cell.js.map +1 -0
  42. package/dist/root.d.ts.map +1 -1
  43. package/dist/root.js +8 -5
  44. package/dist/root.js.map +1 -1
  45. package/package.json +3 -3
  46. package/src/devtools.ts +2 -0
  47. package/src/dom-host-rules.ts +4 -1
  48. package/src/dom-props.ts +2 -1
  49. package/src/element.ts +60 -3
  50. package/src/fiber-commit.ts +3 -1
  51. package/src/fiber-host.ts +2 -0
  52. package/src/fiber.ts +4 -0
  53. package/src/hooks.ts +187 -46
  54. package/src/host-reconciler.ts +1031 -70
  55. package/src/jsx-dev-runtime.ts +4 -0
  56. package/src/jsx-runtime.ts +16 -0
  57. package/src/reactive-prop-cell.ts +67 -0
  58. package/src/root.ts +14 -4
@@ -7,16 +7,25 @@ import {
7
7
  LAZY_TYPE,
8
8
  MEMO_TYPE,
9
9
  Profiler,
10
+ REACTIVE_DOM_BLOCK_TYPE,
10
11
  REACTIVE_TEXT_BINDING_META,
11
12
  STRICT_MODE_TYPE,
12
13
  Suspense,
13
14
  SuspenseList,
14
15
  type ReactCompatElement,
15
16
  type ReactCompatPortal,
17
+ type ReactiveDomBlockProps,
18
+ type ReactiveDomBlockResult,
16
19
  isReactCompatElement,
17
20
  isReactCompatPortal,
18
21
  type ReactCompatNode,
19
22
  } from "./element.js";
23
+ import {
24
+ createReactivePropCell,
25
+ createReactivePropProxy,
26
+ setReactivePropCell,
27
+ type ReactiveDomBlockState,
28
+ } from "./reactive-prop-cell.js";
20
29
  import {
21
30
  consumerContext,
22
31
  isReactCompatConsumer,
@@ -36,6 +45,7 @@ import {
36
45
  namespaceForHostChildren,
37
46
  namespaceForHostElement,
38
47
  type CustomHostDocument,
48
+ type HostElement,
39
49
  type HostNamespace,
40
50
  } from "./dom-host-rules.js";
41
51
  import { createFiber, createWorkInProgress, type Fiber, type FiberRoot } from "./fiber.js";
@@ -96,6 +106,7 @@ interface SuspenseFiberState {
96
106
 
97
107
  const committedPortalContainers = new Set<Element>();
98
108
  const pendingHostRefUpdates: { ref: unknown; node: unknown }[] = [];
109
+ const emptyInstanceKeys: string[] = [];
99
110
 
100
111
  interface FiberHydrationOptions extends RenderOptions {
101
112
  previousNodes?: readonly Node[];
@@ -174,6 +185,10 @@ export function canRenderHostFiber(node: ReactCompatNode): boolean {
174
185
  return true;
175
186
  }
176
187
 
188
+ if (node.type === REACTIVE_DOM_BLOCK_TYPE) {
189
+ return true;
190
+ }
191
+
177
192
  if (isReactCompatProvider(node.type)) {
178
193
  return canRenderHostFiber(node.props.children as ReactCompatNode);
179
194
  }
@@ -258,7 +273,28 @@ export function commitHostFiberRoot(
258
273
  pendingHostRefUpdates.length = 0;
259
274
  const commitPath = getRootCommitPath(options);
260
275
  if (!hasChildListMutation(finishedWork)) {
261
- commitHostDirtyChildren(finishedWork.child, root.container, root.container, commitPath, options);
276
+ commitHostDirtyChildrenOf(
277
+ finishedWork,
278
+ finishedWork.child,
279
+ root.container,
280
+ root.container,
281
+ commitPath,
282
+ options,
283
+ );
284
+ committed = true;
285
+ return;
286
+ }
287
+
288
+ if (
289
+ finishedWork.childListChanged &&
290
+ commitHostKeyedChildListMutationFiber(
291
+ finishedWork,
292
+ root.container,
293
+ root.container,
294
+ commitPath,
295
+ options,
296
+ )
297
+ ) {
262
298
  committed = true;
263
299
  return;
264
300
  }
@@ -317,6 +353,74 @@ export function commitHydratingHostFiberRoot(
317
353
  }
318
354
  }
319
355
 
356
+ export function disposeHostFiberResources(fiber: Fiber | undefined): void {
357
+ if (fiber === undefined || fiber.hasDisposableResources !== true) {
358
+ return;
359
+ }
360
+
361
+ // Dispose this fiber and its SUBTREE only — not its siblings. This is called
362
+ // once per deleted fiber, and deleted siblings are disposed by their own
363
+ // calls; walking siblings here re-walked the whole deleted list per deletion
364
+ // (O(n^2) on a cleared 1k-row reactive list). The dedupe set is allocated
365
+ // once for the subtree walk.
366
+ const seen = new Set<unknown>();
367
+ if (fiber.tag === "reactive-dom-block") {
368
+ disposeReactiveDomBlockState(fiber.stateNode, seen);
369
+ }
370
+ disposeHostFiberChildResources(fiber.child, seen);
371
+ }
372
+
373
+ export function disposeUnretainedHostFiberResources(
374
+ fiber: Fiber | undefined,
375
+ retained: ReadonlySet<Fiber>,
376
+ ): void {
377
+ if (fiber === undefined || fiber.hasDisposableResources !== true || retained.has(fiber)) {
378
+ return;
379
+ }
380
+
381
+ const seen = new Set<unknown>();
382
+ disposeUnretainedHostFiberSubtreeResources(fiber, seen, retained);
383
+ }
384
+
385
+ function disposeHostFiberChildResources(
386
+ fiber: Fiber | undefined,
387
+ seen: Set<unknown>,
388
+ ): void {
389
+ let cursor = fiber;
390
+
391
+ while (cursor !== undefined) {
392
+ if (cursor.hasDisposableResources === true) {
393
+ if (cursor.tag === "reactive-dom-block") {
394
+ disposeReactiveDomBlockState(cursor.stateNode, seen);
395
+ }
396
+
397
+ disposeHostFiberChildResources(cursor.child, seen);
398
+ }
399
+
400
+ cursor = cursor.sibling;
401
+ }
402
+ }
403
+
404
+ function disposeUnretainedHostFiberSubtreeResources(
405
+ fiber: Fiber | undefined,
406
+ seen: Set<unknown>,
407
+ retained: ReadonlySet<Fiber>,
408
+ ): void {
409
+ if (fiber === undefined || retained.has(fiber) || fiber.hasDisposableResources !== true) {
410
+ return;
411
+ }
412
+
413
+ if (fiber.tag === "reactive-dom-block") {
414
+ disposeReactiveDomBlockState(fiber.stateNode, seen);
415
+ }
416
+
417
+ let child = fiber.child;
418
+ while (child !== undefined) {
419
+ disposeUnretainedHostFiberSubtreeResources(child, seen, retained);
420
+ child = child.sibling;
421
+ }
422
+ }
423
+
320
424
  function reconcileHostChild(
321
425
  parent: Fiber,
322
426
  currentFirstChild: Fiber | undefined,
@@ -342,7 +446,15 @@ function reconcileHostChild(
342
446
  const rowResult =
343
447
  children === undefined
344
448
  ? undefined
345
- : reconcileKeyedRowHostChildren(parent, currentFirstChild, children, options);
449
+ : reconcileKeyedRowHostChildren(parent, currentFirstChild, children, options) ??
450
+ reconcileKeyedMemoRowHostChildren(
451
+ parent,
452
+ currentFirstChild,
453
+ children,
454
+ runtime,
455
+ path,
456
+ options,
457
+ );
346
458
  if (rowResult !== undefined) {
347
459
  return rowResult;
348
460
  }
@@ -359,6 +471,7 @@ function reconcileHostChild(
359
471
  let skipRemainingKeyedLookup = false;
360
472
  let sequentialCurrentCursor = currentFirstChild;
361
473
  let childListOrderChanged = false;
474
+ let dirtyChildren: Fiber[] | undefined;
362
475
  let usedCurrentChildren =
363
476
  currentFirstChild === undefined || hasKeyedChildren ? undefined : new Set<Fiber>();
364
477
  const ensureUsedCurrentChildren = (): Set<Fiber> => {
@@ -404,11 +517,28 @@ function reconcileHostChild(
404
517
  currentKeyed?.sibling?.key === key &&
405
518
  canSkipSingleDeletedKeyedFiber(children, index, currentKeyed.sibling)
406
519
  ) {
520
+ const deleted = currentKeyed;
521
+ const matched = currentKeyed.sibling;
522
+ const suffixResult = tryReuseDependencyFreeMemoRemovalSuffix(
523
+ parent,
524
+ children,
525
+ index,
526
+ deleted,
527
+ matched,
528
+ runtime,
529
+ options,
530
+ first,
531
+ previous,
532
+ consumed,
533
+ );
534
+ if (suffixResult !== undefined) {
535
+ return suffixResult;
536
+ }
407
537
  childListOrderChanged = true;
408
538
  ensureUsedCurrentChildren();
409
- matchedCurrent = currentKeyed.sibling;
539
+ matchedCurrent = matched;
410
540
  canReuseMatchedCurrentFiber = false;
411
- currentKeyed = currentKeyed.sibling.sibling;
541
+ currentKeyed = matched.sibling;
412
542
  } else {
413
543
  if (
414
544
  children !== undefined &&
@@ -489,6 +619,14 @@ function reconcileHostChild(
489
619
  && fiber.memoizedState === undefined
490
620
  ) {
491
621
  fiber.memoizedState = index;
622
+ } else if (
623
+ fiber.tag === "memo" &&
624
+ (fiber.stateNode === undefined || typeof fiber.stateNode === "number")
625
+ ) {
626
+ fiber.stateNode = index;
627
+ }
628
+ if (hasHostCommitWork(fiber)) {
629
+ (dirtyChildren ??= []).push(fiber);
492
630
  }
493
631
  previous = fiber;
494
632
  }
@@ -500,6 +638,7 @@ function reconcileHostChild(
500
638
  }
501
639
  parent.childListChanged =
502
640
  childListOrderChanged || childFiberListShapeChanged(currentFirstChild, first);
641
+ recordDirtyChildCommitHints(parent, dirtyChildren);
503
642
 
504
643
  return { fiber: first, consumed };
505
644
  }
@@ -528,6 +667,7 @@ function reconcileKeyedRowHostChildren(
528
667
  let subtreeFlags = NoFlags;
529
668
  let subtreeChildListChanged = false;
530
669
  let hasRefSubtree = false;
670
+ let hasDisposableResources = false;
531
671
  let appendSuffix: AppendSuffixCommitHint | undefined;
532
672
  const canReuseUnchangedRows = hasSameKeyOrderPrefix(currentFirstChild, children);
533
673
  const row = createKeyedRowHostElementScratch();
@@ -593,6 +733,9 @@ function reconcileKeyedRowHostChildren(
593
733
  if (fiber.hasRefSubtree) {
594
734
  hasRefSubtree = true;
595
735
  }
736
+ if (fiber.hasDisposableResources) {
737
+ hasDisposableResources = true;
738
+ }
596
739
  subtreeFlags |= fiber.flags | fiber.subtreeFlags;
597
740
  subtreeChildListChanged =
598
741
  subtreeChildListChanged ||
@@ -610,6 +753,7 @@ function reconcileKeyedRowHostChildren(
610
753
  }
611
754
 
612
755
  parent.hasRefSubtree = hasRefSubtree;
756
+ parent.hasDisposableResources = hasDisposableResources;
613
757
  parent.subtreeFlags = subtreeFlags;
614
758
  parent.subtreeChildListChanged = subtreeChildListChanged;
615
759
  parent.childListChanged = listShapeChanged;
@@ -619,6 +763,382 @@ function reconcileKeyedRowHostChildren(
619
763
  return { fiber: first, consumed: 0 };
620
764
  }
621
765
 
766
+ function isKeyedMemoRowCandidate(node: ReactCompatNode): boolean {
767
+ return (
768
+ isReactCompatElement(node) &&
769
+ node.key !== null &&
770
+ node.ref === null &&
771
+ isMemoType(node.type)
772
+ );
773
+ }
774
+
775
+ // Marker the compiler stamps on a lowered host-only component that returns its
776
+ // props verbatim as a reactive block (`createReactiveDomBlock(render, props)`).
777
+ // Such a component is pure and structurally static, so on a props change the
778
+ // reconciler can drive its committed block straight through the prop cell instead
779
+ // of re-invoking the component (the block's bound DOM updates via subscriptions).
780
+ const STATIC_REACTIVE_BLOCK_MARKER = "__mreactStaticBlock";
781
+
782
+ function isStaticReactiveBlockComponent(type: unknown): boolean {
783
+ return (
784
+ typeof type === "function" &&
785
+ (type as unknown as Record<string, unknown>)[STATIC_REACTIVE_BLOCK_MARKER] === true
786
+ );
787
+ }
788
+
789
+ // In-place bailout for a keyed memo row whose props are unchanged and which has
790
+ // no hook/context/effect dependencies. The caller has proven the key order is
791
+ // unchanged (hasSameKeyOrderPrefix), so the fiber keeps its position and can be
792
+ // reused WITHOUT createWorkInProgress — no allocation, and the retained subtree
793
+ // (including any reactive-dom-block subscriptions) is left untouched. Returns
794
+ // undefined when the row must re-render or has dependencies (handled by the
795
+ // general per-child path instead). Mirrors tryReuseDependencyFreeMemoBailout +
796
+ // getMemoBailoutFiber's in-place branch, but is safe to reuse a fiber with
797
+ // disposable resources because a stable position means nothing is disposed.
798
+ function tryReuseDependencyFreeKeyedMemoRow(
799
+ matched: Fiber,
800
+ child: ReactCompatElement,
801
+ ): Fiber | undefined {
802
+ if (
803
+ matched.tag !== "memo" ||
804
+ matched.type !== child.type ||
805
+ matched.hasRefSubtree === true ||
806
+ matched.hydrateExisting === true
807
+ ) {
808
+ return undefined;
809
+ }
810
+
811
+ const state = matched.memoizedState as MemoFiberState | undefined;
812
+
813
+ if (
814
+ state === undefined ||
815
+ state.hasDirtyInstanceDependencies !== false ||
816
+ state.hasUnflushedEffectDependencies !== false ||
817
+ state.hasRetainedInstanceDependencies !== false ||
818
+ !areMemoPropsEqual(
819
+ child.type as { compare?: (a: Record<string, unknown>, b: Record<string, unknown>) => boolean },
820
+ state.props,
821
+ child.props as Record<string, unknown>,
822
+ )
823
+ ) {
824
+ return undefined;
825
+ }
826
+
827
+ matched.pendingProps = child.props;
828
+ matched.flags = NoFlags;
829
+ matched.subtreeFlags = NoFlags;
830
+ matched.childListChanged = false;
831
+ matched.subtreeChildListChanged = false;
832
+ matched.hostChildListChanged = false;
833
+ matched.child = getSkippedChild(matched);
834
+ return matched;
835
+ }
836
+
837
+ // In-place CHANGED-row update for a keyed memo row whose inner component is a
838
+ // compiler-marked static reactive block (returns its props verbatim as the block
839
+ // props). Instead of re-invoking the component to rebuild a throwaway block
840
+ // element, push the new props straight into the committed block's prop cell — the
841
+ // bound DOM updates via subscriptions, exactly as the normal block re-render path
842
+ // would, but without the memo/component render machinery. Same-order is proven by
843
+ // the caller, so the fiber is reused in place. Returns undefined when this is not
844
+ // a marked, dependency-free, single-block memo row (the general path handles it).
845
+ function tryCellUpdateStaticBlockMemoRow(
846
+ matched: Fiber,
847
+ child: ReactCompatElement,
848
+ ): Fiber | undefined {
849
+ if (
850
+ matched.tag !== "memo" ||
851
+ matched.type !== child.type ||
852
+ matched.hasRefSubtree === true ||
853
+ matched.hydrateExisting === true ||
854
+ !isStaticReactiveBlockComponent((matched.type as { type?: unknown }).type)
855
+ ) {
856
+ return undefined;
857
+ }
858
+
859
+ const state = matched.memoizedState as MemoFiberState | undefined;
860
+
861
+ if (
862
+ state === undefined ||
863
+ state.hasDirtyInstanceDependencies !== false ||
864
+ state.hasUnflushedEffectDependencies !== false ||
865
+ state.hasRetainedInstanceDependencies !== false
866
+ ) {
867
+ return undefined;
868
+ }
869
+
870
+ // Navigate memo -> component fiber -> reactive-dom-block fiber. The marked
871
+ // component renders exactly one static block, so anything else is unexpected.
872
+ const componentFiber = matched.child;
873
+ if (componentFiber === undefined || componentFiber.sibling !== undefined) {
874
+ return undefined;
875
+ }
876
+
877
+ const blockFiber = componentFiber.child;
878
+ if (
879
+ blockFiber === undefined ||
880
+ blockFiber.tag !== "reactive-dom-block" ||
881
+ blockFiber.sibling !== undefined
882
+ ) {
883
+ return undefined;
884
+ }
885
+
886
+ const blockState = blockFiber.stateNode as ReactiveDomBlockState | undefined;
887
+ if (blockState?.propCell === undefined) {
888
+ return undefined;
889
+ }
890
+
891
+ // Drive the bound DOM through the prop cell (the marker guarantees the block's
892
+ // props are the component's props verbatim), then reuse the memo fiber in place.
893
+ setReactivePropCell(blockState.propCell, child.props as Record<string, unknown>);
894
+ matched.memoizedState = { ...state, props: child.props as Record<string, unknown> };
895
+ matched.pendingProps = child.props;
896
+ matched.flags = NoFlags;
897
+ matched.subtreeFlags = NoFlags;
898
+ matched.childListChanged = false;
899
+ matched.subtreeChildListChanged = false;
900
+ matched.hostChildListChanged = false;
901
+ matched.child = getSkippedChild(matched);
902
+ return matched;
903
+ }
904
+
905
+ // Fast path for a keyed list of memo-wrapped rows (e.g. `<RowMemo key={id} />`)
906
+ // whose key order is UNCHANGED between renders — the js-framework-benchmark
907
+ // "select row" and "partial update" shapes. Walks current fibers and new
908
+ // children in lockstep: unchanged rows bail in place (no allocation, no general
909
+ // keyed-reconcile machinery), changed rows re-render through the proven
910
+ // per-child path. Any reorder / insert / delete / non-memo child / length change
911
+ // makes it return undefined so the general reconcile takes over.
912
+ function reconcileKeyedMemoRowHostChildren(
913
+ parent: Fiber,
914
+ currentFirstChild: Fiber | undefined,
915
+ children: readonly ReactCompatNode[],
916
+ runtime: RootRuntime | undefined,
917
+ path: string,
918
+ options: FiberHydrationOptions,
919
+ ): FiberReconcileResult | undefined {
920
+ if (
921
+ children.length === 0 ||
922
+ currentFirstChild === undefined ||
923
+ runtime === undefined ||
924
+ options.previousNodes !== undefined ||
925
+ !isKeyedMemoRowCandidate(children[0]) ||
926
+ !hasSameKeyOrderPrefix(currentFirstChild, children)
927
+ ) {
928
+ return undefined;
929
+ }
930
+
931
+ let currentKeyed: Fiber | undefined = currentFirstChild;
932
+ let first: Fiber | undefined;
933
+ let previous: Fiber | undefined;
934
+ let subtreeFlags = NoFlags;
935
+ let subtreeChildListChanged = false;
936
+ let hasRefSubtree = false;
937
+ let hasDisposableResources = false;
938
+ let dirtyChildren: Fiber[] | undefined;
939
+
940
+ for (let index = 0; index < children.length; index += 1) {
941
+ const child = children[index];
942
+
943
+ if (currentKeyed === undefined || !isKeyedMemoRowCandidate(child)) {
944
+ return undefined;
945
+ }
946
+
947
+ const matched = currentKeyed;
948
+ currentKeyed = currentKeyed.sibling;
949
+ const childElement = child as ReactCompatElement;
950
+
951
+ let fiber =
952
+ tryReuseDependencyFreeKeyedMemoRow(matched, childElement) ??
953
+ tryCellUpdateStaticBlockMemoRow(matched, childElement);
954
+ if (fiber === undefined) {
955
+ const result = createHostFiber(
956
+ parent,
957
+ matched,
958
+ child,
959
+ childElement.key ?? undefined,
960
+ runtime,
961
+ getReconcileChildPath(path, child, index, options),
962
+ options,
963
+ false,
964
+ );
965
+ fiber = result.fiber;
966
+
967
+ if (fiber === undefined) {
968
+ return undefined;
969
+ }
970
+
971
+ if (fiber !== matched && fiber.alternate !== matched) {
972
+ markOptimizedChildForDeletion(parent, matched);
973
+ }
974
+ }
975
+
976
+ if (first === undefined) {
977
+ first = fiber;
978
+ } else if (previous !== undefined) {
979
+ previous.sibling = fiber;
980
+ }
981
+
982
+ fiber.return = parent;
983
+ fiber.sibling = undefined;
984
+ if (fiber.hasRefSubtree) {
985
+ hasRefSubtree = true;
986
+ }
987
+ if (fiber.hasDisposableResources) {
988
+ hasDisposableResources = true;
989
+ }
990
+ subtreeFlags |= fiber.flags | fiber.subtreeFlags;
991
+ subtreeChildListChanged =
992
+ subtreeChildListChanged || fiber.childListChanged || fiber.subtreeChildListChanged;
993
+ if (
994
+ fiber.tag === "memo" &&
995
+ (fiber.stateNode === undefined || typeof fiber.stateNode === "number")
996
+ ) {
997
+ fiber.stateNode = index;
998
+ }
999
+ if (hasHostCommitWork(fiber)) {
1000
+ (dirtyChildren ??= []).push(fiber);
1001
+ }
1002
+ previous = fiber;
1003
+ }
1004
+
1005
+ if (currentKeyed !== undefined) {
1006
+ return undefined;
1007
+ }
1008
+
1009
+ parent.hasRefSubtree = hasRefSubtree;
1010
+ parent.hasDisposableResources = hasDisposableResources;
1011
+ parent.subtreeFlags = subtreeFlags;
1012
+ parent.subtreeChildListChanged = subtreeChildListChanged;
1013
+ parent.childListChanged = false;
1014
+ recordDirtyChildCommitHints(parent, dirtyChildren);
1015
+ return { fiber: first, consumed: 0 };
1016
+ }
1017
+
1018
+ function canReuseDependencyFreeMemoAtKey(
1019
+ current: Fiber | undefined,
1020
+ node: ReactCompatElement,
1021
+ key: string,
1022
+ ): boolean {
1023
+ return (
1024
+ current !== undefined &&
1025
+ current.key === key &&
1026
+ current.tag === "memo" &&
1027
+ current.type === node.type &&
1028
+ current.hasRefSubtree !== true &&
1029
+ current.hasDisposableResources !== true &&
1030
+ current.hydrateExisting !== true &&
1031
+ isMemoType(node.type)
1032
+ );
1033
+ }
1034
+
1035
+ function tryReuseDependencyFreeMemoRemovalSuffix(
1036
+ parent: Fiber,
1037
+ children: readonly ReactCompatNode[],
1038
+ startIndex: number,
1039
+ removed: Fiber,
1040
+ matchedCurrent: Fiber,
1041
+ runtime: RootRuntime | undefined,
1042
+ options: FiberHydrationOptions,
1043
+ prefixFirst: Fiber | undefined,
1044
+ prefixPrevious: Fiber | undefined,
1045
+ consumed: number,
1046
+ ): FiberReconcileResult | undefined {
1047
+ if (
1048
+ runtime === undefined ||
1049
+ options.previousNodes !== undefined ||
1050
+ removed.hasRefSubtree
1051
+ ) {
1052
+ return undefined;
1053
+ }
1054
+
1055
+ const suffix: Fiber[] = [];
1056
+ let current: Fiber | undefined = matchedCurrent;
1057
+
1058
+ for (let index = startIndex; index < children.length; index += 1) {
1059
+ const child = children[index];
1060
+ const key = getNodeKey(child);
1061
+
1062
+ if (key === undefined || !isReactCompatElement(child) || !isMemoType(child.type)) {
1063
+ return undefined;
1064
+ }
1065
+
1066
+ if (!canReuseDependencyFreeMemoAtKey(current, child, key)) {
1067
+ return undefined;
1068
+ }
1069
+
1070
+ if (current === undefined) {
1071
+ return undefined;
1072
+ }
1073
+
1074
+ const matched: Fiber = current;
1075
+ const fiber = reuseDependencyFreeMemoFiber(matched, child);
1076
+
1077
+ if (fiber === undefined) {
1078
+ return undefined;
1079
+ }
1080
+
1081
+ suffix.push(fiber);
1082
+ current = matched.sibling;
1083
+ }
1084
+
1085
+ if (current !== undefined) {
1086
+ return undefined;
1087
+ }
1088
+
1089
+ let first = prefixFirst;
1090
+ let previous = prefixPrevious;
1091
+
1092
+ for (const fiber of suffix) {
1093
+ if (first === undefined) {
1094
+ first = fiber;
1095
+ } else if (previous !== undefined) {
1096
+ previous.sibling = fiber;
1097
+ }
1098
+
1099
+ fiber.return = parent;
1100
+ fiber.sibling = undefined;
1101
+ previous = fiber;
1102
+ }
1103
+
1104
+ parent.childListChanged = true;
1105
+ parent.deletions = [removed];
1106
+ markOptimizedChildForDeletion(parent, removed);
1107
+ return { fiber: first, consumed };
1108
+ }
1109
+
1110
+ function reuseDependencyFreeMemoFiber(
1111
+ current: Fiber,
1112
+ node: ReactCompatElement,
1113
+ ): Fiber | undefined {
1114
+ if (!isMemoType(node.type)) {
1115
+ return undefined;
1116
+ }
1117
+
1118
+ const previousMemoState = current.memoizedState as MemoFiberState | undefined;
1119
+
1120
+ if (
1121
+ previousMemoState === undefined ||
1122
+ previousMemoState.hasDirtyInstanceDependencies !== false ||
1123
+ previousMemoState.hasUnflushedEffectDependencies !== false ||
1124
+ previousMemoState.hasRetainedInstanceDependencies !== false ||
1125
+ !areMemoPropsEqual(node.type, previousMemoState.props, node.props)
1126
+ ) {
1127
+ return undefined;
1128
+ }
1129
+
1130
+ current.pendingProps = node.props;
1131
+ current.flags = NoFlags;
1132
+ current.subtreeFlags = NoFlags;
1133
+ current.childListChanged = false;
1134
+ current.subtreeChildListChanged = false;
1135
+ current.hostChildListChanged = false;
1136
+ current.type = node.type;
1137
+ current.child = getSkippedChild(current);
1138
+ current.memoizedState = previousMemoState;
1139
+ return current;
1140
+ }
1141
+
622
1142
  function canStoreAppendSuffixCommitHint(parent: Fiber): boolean {
623
1143
  return (
624
1144
  parent.tag === "fragment" ||
@@ -712,6 +1232,7 @@ function getReusableKeyedRowHostFiber(
712
1232
  current.subtreeChildListChanged = false;
713
1233
  current.hostChildListChanged = false;
714
1234
  current.hasRefSubtree = false;
1235
+ current.hasDisposableResources = false;
715
1236
  return current;
716
1237
  }
717
1238
 
@@ -793,6 +1314,7 @@ function createKeyedRowHostFiber(
793
1314
  fiber.pendingProps = node.props;
794
1315
  fiber.hostChildListChanged = false;
795
1316
  fiber.hasRefSubtree = false;
1317
+ fiber.hasDisposableResources = false;
796
1318
 
797
1319
  if (current === undefined || fiber.alternate !== current) {
798
1320
  fiber.flags |= Placement;
@@ -956,6 +1478,9 @@ function bubbleHostChild(parent: Fiber, child: Fiber): void {
956
1478
  if (child.hasRefSubtree) {
957
1479
  parent.hasRefSubtree = true;
958
1480
  }
1481
+ if (child.hasDisposableResources) {
1482
+ parent.hasDisposableResources = true;
1483
+ }
959
1484
  parent.subtreeFlags |= child.flags | child.subtreeFlags;
960
1485
  parent.subtreeChildListChanged =
961
1486
  parent.subtreeChildListChanged ||
@@ -963,8 +1488,21 @@ function bubbleHostChild(parent: Fiber, child: Fiber): void {
963
1488
  child.subtreeChildListChanged;
964
1489
  }
965
1490
 
1491
+ function recordDirtyChildCommitHints(
1492
+ parent: Fiber,
1493
+ dirtyChildren: Fiber[] | undefined,
1494
+ ): void {
1495
+ if (parent.childListChanged || dirtyChildren === undefined || dirtyChildren.length === 0) {
1496
+ return;
1497
+ }
1498
+
1499
+ // Reuse the effect-list slot only when there are no child-list deletions.
1500
+ parent.deletions = dirtyChildren;
1501
+ }
1502
+
966
1503
  function resetFiberRefSubtree(fiber: Fiber): void {
967
1504
  fiber.hasRefSubtree = false;
1505
+ fiber.hasDisposableResources = false;
968
1506
  }
969
1507
 
970
1508
  function includeNodeRef(fiber: Fiber, node: ReactCompatNode): void {
@@ -1142,6 +1680,13 @@ function createHostFiberImpl(
1142
1680
  return { fiber: undefined, consumed: 0 };
1143
1681
  }
1144
1682
 
1683
+ // Host elements (string type) are by far the most common node. Dispatch them
1684
+ // before the component-type checks below, none of which a string can match,
1685
+ // so each reconciled host element skips ~12 type comparisons / probes.
1686
+ if (typeof node.type === "string") {
1687
+ return createHostComponentFiber(parent, current, node, key, runtime, path, options);
1688
+ }
1689
+
1145
1690
  if (node.type === Fragment) {
1146
1691
  const fiber =
1147
1692
  current?.tag === "fragment"
@@ -1269,6 +1814,37 @@ function createHostFiberImpl(
1269
1814
  return { fiber, consumed: options.previousNodes?.length ?? 0 };
1270
1815
  }
1271
1816
 
1817
+ if (node.type === REACTIVE_DOM_BLOCK_TYPE) {
1818
+ const blockProps = (node.props as unknown as ReactiveDomBlockProps).blockProps;
1819
+ if (current?.tag === "reactive-dom-block") {
1820
+ // Re-render: reuse the committed DOM/subscriptions and push the new props
1821
+ // into the prop cell instead of re-running render(). Bound text/attributes
1822
+ // update via their reactive subscriptions; the subtree is never reconciled.
1823
+ const fiber = createWorkInProgress(current, node.props);
1824
+ fiber.type = node.type;
1825
+ fiber.hasDisposableResources = true;
1826
+ const previousState = current.stateNode as ReactiveDomBlockState | undefined;
1827
+ if (previousState?.propCell !== undefined && blockProps !== undefined) {
1828
+ setReactivePropCell(previousState.propCell, blockProps);
1829
+ }
1830
+ fiber.stateNode = previousState;
1831
+ return { fiber, consumed: options.previousNodes?.length ?? 0 };
1832
+ }
1833
+
1834
+ const fiber = createFiber("reactive-dom-block", node.props, key);
1835
+ fiber.type = node.type;
1836
+ fiber.hasDisposableResources = true;
1837
+ const render = (node.props as unknown as ReactiveDomBlockProps).render;
1838
+ if (blockProps !== undefined) {
1839
+ const propCell = createReactivePropCell(blockProps);
1840
+ const result = render(createReactivePropProxy(propCell));
1841
+ fiber.stateNode = { node: result.node, dispose: result.dispose, propCell };
1842
+ } else {
1843
+ fiber.stateNode = (render as () => ReactiveDomBlockResult)();
1844
+ }
1845
+ return { fiber, consumed: options.previousNodes?.length ?? 0 };
1846
+ }
1847
+
1272
1848
  if (isReactCompatProvider(node.type)) {
1273
1849
  const fiber =
1274
1850
  current?.tag === "context-provider" && current.type === node.type
@@ -1390,8 +1966,11 @@ function createHostFiberImpl(
1390
1966
  fiber.type = memoType;
1391
1967
 
1392
1968
  const renderedElement: ReactCompatElement = {
1393
- ...node,
1969
+ $$typeof: node.$$typeof,
1394
1970
  type: memoType.type,
1971
+ key: node.key,
1972
+ ref: node.ref,
1973
+ props: node.props,
1395
1974
  };
1396
1975
  const childResult = createHostFiber(
1397
1976
  fiber,
@@ -1408,7 +1987,7 @@ function createHostFiberImpl(
1408
1987
  fiber.child.sibling = undefined;
1409
1988
  bubbleHostChild(fiber, fiber.child);
1410
1989
  }
1411
- const instanceKeys = collectInstanceKeys(runtime, memoPath);
1990
+ const instanceKeys = collectMemoInstanceKeys(runtime, memoPath);
1412
1991
  const hasClassDescendant = hasClassComponentDescendant(fiber.child);
1413
1992
  fiber.memoizedState = {
1414
1993
  props: node.props as Record<string, unknown>,
@@ -1439,8 +2018,11 @@ function createHostFiberImpl(
1439
2018
 
1440
2019
  if (lazyType.status === "resolved" && lazyType.resolved !== undefined) {
1441
2020
  const renderedElement: ReactCompatElement = {
1442
- ...node,
2021
+ $$typeof: node.$$typeof,
1443
2022
  type: lazyType.resolved,
2023
+ key: node.key,
2024
+ ref: node.ref,
2025
+ props: node.props,
1444
2026
  };
1445
2027
  const childResult = createHostFiber(
1446
2028
  fiber,
@@ -1625,61 +2207,95 @@ function createHostFiberImpl(
1625
2207
  const instanceKeys = collectInstanceKeys(runtime, path);
1626
2208
  fiber.stateNode = {
1627
2209
  element: node,
1628
- props: { ...node.props },
2210
+ props: node.props as Record<string, unknown>,
1629
2211
  instanceKeys,
1630
2212
  hasContextDependencies: hasContextDependency(runtime, instanceKeys),
1631
2213
  } satisfies FunctionFiberState;
1632
2214
  return { fiber, consumed: childResult.consumed };
1633
2215
  }
1634
2216
 
2217
+ return { fiber: undefined, consumed: 0 };
2218
+ }
2219
+
2220
+ // The host-component reconcile, split out of createHostFiberImpl so host
2221
+ // elements can be dispatched before the component-type checks. Only called with
2222
+ // a string element type.
2223
+ function createHostComponentFiber(
2224
+ parent: Fiber,
2225
+ current: Fiber | undefined,
2226
+ node: ReactCompatElement,
2227
+ key: string | undefined,
2228
+ runtime: RootRuntime | undefined,
2229
+ path: string,
2230
+ options: FiberHydrationOptions,
2231
+ ): FiberReconcileResult {
1635
2232
  if (typeof node.type !== "string") {
1636
2233
  return { fiber: undefined, consumed: 0 };
1637
2234
  }
1638
2235
 
2236
+ const initialHostOnlyFiber = tryCreateInitialHostOnlyFiber(
2237
+ current,
2238
+ node,
2239
+ key,
2240
+ options,
2241
+ );
2242
+ if (initialHostOnlyFiber !== undefined) {
2243
+ return { fiber: initialHostOnlyFiber, consumed: 0 };
2244
+ }
2245
+
1639
2246
  const elementNamespace = namespaceForHostElement(options.namespace ?? "html", node.type);
1640
2247
  const childNamespace = namespaceForHostChildren(elementNamespace, node.type);
2248
+ const reusableCurrent =
2249
+ current?.tag === "host-component" && current.type === node.type ? current : undefined;
1641
2250
  const fiber =
1642
- current?.tag === "host-component" && current.type === node.type
1643
- ? createWorkInProgress(current, node.props)
2251
+ reusableCurrent !== undefined
2252
+ ? createWorkInProgress(reusableCurrent, node.props)
1644
2253
  : createFiber("host-component", node.props, key);
2254
+ // The hydration node bookkeeping only matters when hydrating. Skipping it for
2255
+ // the common (non-hydration) render avoids an isHostElement(undefined) probe
2256
+ // and the mismatch checks on every reconciled element.
1645
2257
  const existing = options.previousNodes?.[0];
1646
- const existingElement = isHostElement(existing) ? existing : undefined;
1647
- const tagMatches =
1648
- existingElement !== undefined &&
1649
- hostElementMatches(existingElement, node.type, elementNamespace);
1650
-
1651
- if (existing === undefined && options.previousNodes !== undefined) {
1652
- reportMissingHydrationNode(options, path);
1653
- } else if (existing !== undefined && !isHostElement(existing)) {
1654
- reportHydrationNodeTypeMismatch(options, path, `<${node.type}>`, existing);
1655
- }
2258
+ let existingElement: HostElement | undefined;
2259
+ let tagMatches = false;
2260
+ if (options.previousNodes !== undefined) {
2261
+ existingElement = isHostElement(existing) ? existing : undefined;
2262
+ tagMatches =
2263
+ existingElement !== undefined &&
2264
+ hostElementMatches(existingElement, node.type, elementNamespace);
2265
+
2266
+ if (existing === undefined) {
2267
+ reportMissingHydrationNode(options, path);
2268
+ } else if (!isHostElement(existing)) {
2269
+ reportHydrationNodeTypeMismatch(options, path, `<${node.type}>`, existing);
2270
+ }
1656
2271
 
1657
- if (existingElement !== undefined && !tagMatches) {
1658
- reportRecoverable(
1659
- options,
1660
- "tag",
1661
- path,
1662
- new Error(
1663
- `Hydration tag mismatch: expected <${node.type}> but found <${existingElement.tagName.toLowerCase()}>.`,
1664
- ),
1665
- );
1666
- reportElementTextMismatch(options, `${path}.c`, existingElement, node.props.children);
2272
+ if (existingElement !== undefined && !tagMatches) {
2273
+ reportRecoverable(
2274
+ options,
2275
+ "tag",
2276
+ path,
2277
+ new Error(
2278
+ `Hydration tag mismatch: expected <${node.type}> but found <${existingElement.tagName.toLowerCase()}>.`,
2279
+ ),
2280
+ );
2281
+ reportElementTextMismatch(options, `${path}.c`, existingElement, node.props.children);
2282
+ }
1667
2283
  }
1668
2284
 
1669
2285
  fiber.type = node.type;
2286
+ // When reusing a same-type current fiber, its stateNode was created for this
2287
+ // exact tag and namespace (same tree position), so the hostElementMatches
2288
+ // re-check (localName + namespaceURI reads) is redundant.
1670
2289
  fiber.stateNode =
1671
2290
  tagMatches
1672
2291
  ? existingElement
1673
- : current?.tag === "host-component" &&
1674
- current.type === node.type &&
1675
- isHostElement(current.stateNode) &&
1676
- hostElementMatches(current.stateNode, node.type, elementNamespace)
1677
- ? current.stateNode
2292
+ : reusableCurrent !== undefined && isHostElement(reusableCurrent.stateNode)
2293
+ ? reusableCurrent.stateNode
1678
2294
  : createHostElement(getDocumentRef(options), node.type, options.namespace ?? "html");
1679
- fiber.hydrateExisting = tagMatches && options.previousNodes !== undefined;
2295
+ fiber.hydrateExisting = tagMatches;
1680
2296
  const previousChildNodes =
1681
2297
  tagMatches && existingElement !== undefined
1682
- ? Array.from(existingElement.childNodes)
2298
+ ? Array.from((existingElement as Element).childNodes)
1683
2299
  : undefined;
1684
2300
  const directTextChild =
1685
2301
  shouldUseDirectHostTextChild() && previousChildNodes === undefined
@@ -1713,11 +2329,7 @@ function createHostFiberImpl(
1713
2329
  node.props.children as ReactCompatNode,
1714
2330
  runtime,
1715
2331
  `${path}.c`,
1716
- {
1717
- ...options,
1718
- namespace: childNamespace,
1719
- ...(previousChildNodes === undefined ? {} : { previousNodes: previousChildNodes }),
1720
- },
2332
+ getHostChildFiberOptions(options, childNamespace, previousChildNodes),
1721
2333
  );
1722
2334
  fiber.child = childResult.fiber;
1723
2335
  if (previousChildNodes !== undefined) {
@@ -1737,6 +2349,185 @@ function isFunctionComponentType(value: unknown): value is (
1737
2349
  );
1738
2350
  }
1739
2351
 
2352
+ function tryCreateInitialHostOnlyFiber(
2353
+ current: Fiber | undefined,
2354
+ node: ReactCompatElement,
2355
+ key: string | undefined,
2356
+ options: FiberHydrationOptions,
2357
+ ): Fiber | undefined {
2358
+ if (
2359
+ current !== undefined ||
2360
+ options.previousNodes !== undefined ||
2361
+ !shouldUseDirectHostTextChild() ||
2362
+ typeof node.type !== "string" ||
2363
+ !canCreateInitialHostOnlyNode(node)
2364
+ ) {
2365
+ return undefined;
2366
+ }
2367
+
2368
+ return createInitialHostOnlyElementFiber(
2369
+ node,
2370
+ key,
2371
+ options.namespace ?? "html",
2372
+ getDocumentRef(options),
2373
+ );
2374
+ }
2375
+
2376
+ function createInitialHostOnlyElementFiber(
2377
+ node: ReactCompatElement,
2378
+ key: string | undefined,
2379
+ namespace: HostNamespace,
2380
+ documentRef: Document | CustomHostDocument,
2381
+ ): Fiber {
2382
+ const elementType = node.type as string;
2383
+ const elementNamespace = namespaceForHostElement(namespace, elementType);
2384
+ const childNamespace = namespaceForHostChildren(elementNamespace, elementType);
2385
+ const fiber = createFiber("host-component", node.props, key);
2386
+ fiber.type = elementType;
2387
+ fiber.stateNode = createHostElement(documentRef, elementType, namespace);
2388
+ fiber.flags |= Placement;
2389
+ fiber.hostChildListChanged = true;
2390
+
2391
+ if (getDirectHostTextChild(node.props.children) === undefined) {
2392
+ fiber.child = createInitialHostOnlyChildList(
2393
+ fiber,
2394
+ node.props.children as ReactCompatNode,
2395
+ childNamespace,
2396
+ documentRef,
2397
+ );
2398
+ }
2399
+
2400
+ return fiber;
2401
+ }
2402
+
2403
+ function createInitialHostOnlyChildList(
2404
+ parent: Fiber,
2405
+ children: ReactCompatNode,
2406
+ namespace: HostNamespace,
2407
+ documentRef: Document | CustomHostDocument,
2408
+ ): Fiber | undefined {
2409
+ const normalized = normalizeChildren(children);
2410
+ let first: Fiber | undefined;
2411
+ let previous: Fiber | undefined;
2412
+
2413
+ for (let index = 0; index < normalized.length; index += 1) {
2414
+ const child = normalized[index];
2415
+ const fiber = createInitialHostOnlyChildFiber(child, namespace, documentRef);
2416
+
2417
+ if (fiber === undefined) {
2418
+ continue;
2419
+ }
2420
+
2421
+ fiber.return = parent;
2422
+ fiber.memoizedState = index;
2423
+
2424
+ if (first === undefined) {
2425
+ first = fiber;
2426
+ } else if (previous !== undefined) {
2427
+ previous.sibling = fiber;
2428
+ }
2429
+
2430
+ bubbleHostChild(parent, fiber);
2431
+ previous = fiber;
2432
+ }
2433
+
2434
+ return first;
2435
+ }
2436
+
2437
+ function createInitialHostOnlyChildFiber(
2438
+ node: ReactCompatNode,
2439
+ namespace: HostNamespace,
2440
+ documentRef: Document | CustomHostDocument,
2441
+ ): Fiber | undefined {
2442
+ if (node === null || node === undefined || typeof node === "boolean") {
2443
+ return undefined;
2444
+ }
2445
+
2446
+ if (typeof node === "string" || typeof node === "number") {
2447
+ const fiber = createFiber("host-text", String(node));
2448
+ fiber.stateNode = createHostTextNode(documentRef);
2449
+ fiber.flags |= Placement;
2450
+ return fiber;
2451
+ }
2452
+
2453
+ if (Array.isArray(node)) {
2454
+ const fiber = createFiber("fragment", node);
2455
+ fiber.child = createInitialHostOnlyChildList(fiber, node, namespace, documentRef);
2456
+ return fiber.child === undefined ? undefined : fiber;
2457
+ }
2458
+
2459
+ if (!isReactCompatElement(node) || typeof node.type !== "string") {
2460
+ return undefined;
2461
+ }
2462
+
2463
+ return createInitialHostOnlyElementFiber(
2464
+ node,
2465
+ node.key === null ? undefined : node.key,
2466
+ namespace,
2467
+ documentRef,
2468
+ );
2469
+ }
2470
+
2471
+ function canCreateInitialHostOnlyNode(node: ReactCompatNode): boolean {
2472
+ if (
2473
+ node === null ||
2474
+ node === undefined ||
2475
+ typeof node === "boolean" ||
2476
+ typeof node === "string" ||
2477
+ typeof node === "number"
2478
+ ) {
2479
+ return true;
2480
+ }
2481
+
2482
+ if (Array.isArray(node)) {
2483
+ return node.every(canCreateInitialHostOnlyNode);
2484
+ }
2485
+
2486
+ if (
2487
+ !isReactCompatElement(node) ||
2488
+ typeof node.type !== "string" ||
2489
+ node.ref !== null ||
2490
+ hasInitialHostOnlyExcludedProps(node.props)
2491
+ ) {
2492
+ return false;
2493
+ }
2494
+
2495
+ return canCreateInitialHostOnlyNode(node.props.children as ReactCompatNode);
2496
+ }
2497
+
2498
+ function hasInitialHostOnlyExcludedProps(props: Record<string, unknown>): boolean {
2499
+ return (
2500
+ hasOwnProperty.call(props, REACTIVE_TEXT_BINDING_META) ||
2501
+ props.dangerouslySetInnerHTML !== undefined ||
2502
+ props.contentEditable === true ||
2503
+ props.suppressContentEditableWarning === true ||
2504
+ props.value !== undefined ||
2505
+ props.defaultValue !== undefined ||
2506
+ props.checked !== undefined ||
2507
+ props.defaultChecked !== undefined
2508
+ );
2509
+ }
2510
+
2511
+ function getHostChildFiberOptions(
2512
+ options: FiberHydrationOptions,
2513
+ namespace: HostNamespace,
2514
+ previousNodes: readonly Node[] | undefined,
2515
+ ): FiberHydrationOptions {
2516
+ const namespaceUnchanged =
2517
+ options.namespace === namespace ||
2518
+ (options.namespace === undefined && namespace === "html");
2519
+
2520
+ if (previousNodes === undefined && namespaceUnchanged) {
2521
+ return options;
2522
+ }
2523
+
2524
+ return {
2525
+ ...options,
2526
+ namespace,
2527
+ ...(previousNodes === undefined ? {} : { previousNodes }),
2528
+ };
2529
+ }
2530
+
1740
2531
  function commitHostChildren(
1741
2532
  fiber: Fiber | undefined,
1742
2533
  parent: ParentNode,
@@ -1778,6 +2569,49 @@ function commitHostDirtyChildren(
1778
2569
  }
1779
2570
  }
1780
2571
 
2572
+ function commitHostDirtyChildrenOf(
2573
+ owner: Fiber,
2574
+ fiber: Fiber | undefined,
2575
+ parent: ParentNode,
2576
+ eventRoot: Element,
2577
+ path: string,
2578
+ options: RenderOptions = {},
2579
+ ): void {
2580
+ const dirtyChildren = readDirtyChildCommitHints(owner);
2581
+
2582
+ if (dirtyChildren === undefined) {
2583
+ commitHostDirtyChildren(fiber, parent, eventRoot, path, options);
2584
+ return;
2585
+ }
2586
+
2587
+ for (let index = 0; index < dirtyChildren.length; index += 1) {
2588
+ const dirtyChild = dirtyChildren[index];
2589
+
2590
+ if (dirtyChild !== undefined && hasHostCommitWork(dirtyChild)) {
2591
+ commitHostDirtyFiber(
2592
+ dirtyChild,
2593
+ parent,
2594
+ eventRoot,
2595
+ joinCommitPath(path, String(getDirtyChildCommitIndex(dirtyChild, index))),
2596
+ options,
2597
+ );
2598
+ }
2599
+ }
2600
+ owner.deletions = undefined;
2601
+ }
2602
+
2603
+ function readDirtyChildCommitHints(fiber: Fiber): Fiber[] | undefined {
2604
+ return fiber.childListChanged ? undefined : fiber.deletions;
2605
+ }
2606
+
2607
+ function getDirtyChildCommitIndex(fiber: Fiber, fallback: number): number {
2608
+ if (typeof fiber.memoizedState === "number") {
2609
+ return fiber.memoizedState;
2610
+ }
2611
+
2612
+ return typeof fiber.stateNode === "number" ? fiber.stateNode : fallback;
2613
+ }
2614
+
1781
2615
  function commitHostDirtyFiber(
1782
2616
  fiber: Fiber,
1783
2617
  parent: ParentNode,
@@ -1798,6 +2632,7 @@ function commitHostDirtyFiber(
1798
2632
  return;
1799
2633
  }
1800
2634
 
2635
+ const isDomElement = isDomHostElement(element);
1801
2636
  const props = fiber.pendingProps as Record<string, unknown>;
1802
2637
  const previousProps = fiber.memoizedProps as Record<string, unknown> | undefined;
1803
2638
  const directTextChild =
@@ -1823,7 +2658,7 @@ function commitHostDirtyFiber(
1823
2658
  fiber.hydrateExisting !== true &&
1824
2659
  isRowTextOnlyUpdate(fiber.memoizedProps, props);
1825
2660
 
1826
- if (isDomHostElement(element) && !propsAreUnchanged && !propsAreChildrenOnly && !textOnlyRowUpdate) {
2661
+ if (isDomElement && !propsAreUnchanged && !propsAreChildrenOnly && !textOnlyRowUpdate) {
1827
2662
  applyProps(element, props, path, {
1828
2663
  ...options,
1829
2664
  eventRoot,
@@ -1841,16 +2676,16 @@ function commitHostDirtyFiber(
1841
2676
  ) {
1842
2677
  const childNodes = commitHostChildren(fiber.child, element, eventRoot, `${path}.c`, options);
1843
2678
  if (
1844
- !(isDomHostElement(element) && childNodes.length === 0 && committedPortalContainers.has(element)) &&
1845
- !(isDomHostElement(element) && shouldPreserveContentEditableChildren(element, props, childNodes))
2679
+ !(isDomElement && childNodes.length === 0 && committedPortalContainers.has(element)) &&
2680
+ !(isDomElement && shouldPreserveContentEditableChildren(element, props, childNodes))
1846
2681
  ) {
1847
2682
  syncChildNodes(element as ParentNode, childNodes);
1848
2683
  }
1849
2684
  } else if (fiber.subtreeFlags !== NoFlags) {
1850
- commitHostDirtyChildren(fiber.child, element, eventRoot, `${path}.c`, options);
2685
+ commitHostDirtyChildrenOf(fiber, fiber.child, element, eventRoot, `${path}.c`, options);
1851
2686
  }
1852
2687
 
1853
- if (isDomHostElement(element)) {
2688
+ if (isDomElement && isFormHostType(fiber.type)) {
1854
2689
  applyPostChildFormProps(element, props, previousProps);
1855
2690
  }
1856
2691
  applyChangedRef(previousProps?.ref, props.ref, element);
@@ -1872,7 +2707,7 @@ function commitHostDirtyFiber(
1872
2707
  : container instanceof Element
1873
2708
  ? container
1874
2709
  : eventRoot;
1875
- const portalOptions = withPortalDocumentRef(options, container);
2710
+ const portalOptions = withPortalHostOptions(options, container);
1876
2711
 
1877
2712
  if (
1878
2713
  fiber.childListChanged ||
@@ -1890,7 +2725,8 @@ function commitHostDirtyFiber(
1890
2725
  syncOwnedChildNodes(container as ParentNode, previousNodes, childNodes);
1891
2726
  fiber.memoizedState = childNodes;
1892
2727
  } else {
1893
- commitHostDirtyChildren(
2728
+ commitHostDirtyChildrenOf(
2729
+ fiber,
1894
2730
  fiber.child,
1895
2731
  container as ParentNode,
1896
2732
  portalEventRoot,
@@ -1904,8 +2740,14 @@ function commitHostDirtyFiber(
1904
2740
  return;
1905
2741
  }
1906
2742
 
2743
+ if (fiber.tag === "reactive-dom-block") {
2744
+ fiber.memoizedProps = fiber.pendingProps;
2745
+ finishCommittedFiber(fiber);
2746
+ return;
2747
+ }
2748
+
1907
2749
  if (fiber.subtreeFlags !== NoFlags) {
1908
- commitHostDirtyChildren(fiber.child, parent, eventRoot, path, options);
2750
+ commitHostDirtyChildrenOf(fiber, fiber.child, parent, eventRoot, path, options);
1909
2751
  }
1910
2752
  fiber.memoizedProps = fiber.pendingProps;
1911
2753
  finishCommittedFiber(fiber);
@@ -2072,7 +2914,10 @@ function readAppendSuffixCommitHint(value: unknown): AppendSuffixCommitHint | un
2072
2914
  }
2073
2915
 
2074
2916
  function commitHostSingleRemoval(fiber: Fiber, parent: ParentNode): boolean {
2075
- const removed = getSingleRemovedFiber(fiber.alternate?.child, fiber.child);
2917
+ const removed =
2918
+ fiber.deletions?.length === 1
2919
+ ? fiber.deletions[0]
2920
+ : getSingleRemovedFiber(fiber.alternate?.child, fiber.child);
2076
2921
 
2077
2922
  if (removed === undefined) {
2078
2923
  return false;
@@ -2089,6 +2934,10 @@ function commitHostSingleRemoval(fiber: Fiber, parent: ParentNode): boolean {
2089
2934
  removedAny = true;
2090
2935
  }
2091
2936
 
2937
+ if (removedAny) {
2938
+ fiber.deletions = undefined;
2939
+ }
2940
+
2092
2941
  return removedAny;
2093
2942
  }
2094
2943
 
@@ -2166,6 +3015,11 @@ function isSameFiberSlot(current: Fiber, next: Fiber): boolean {
2166
3015
  }
2167
3016
 
2168
3017
  function collectCommittedHostNodes(fiber: Fiber): Node[] {
3018
+ if (fiber.tag === "reactive-dom-block") {
3019
+ const node = getReactiveDomBlockNode(fiber.stateNode);
3020
+ return node === undefined ? [] : [node];
3021
+ }
3022
+
2169
3023
  if (
2170
3024
  (fiber.tag === "host-component" || fiber.tag === "host-text") &&
2171
3025
  fiber.stateNode instanceof Node
@@ -2185,6 +3039,10 @@ function collectCommittedHostNodes(fiber: Fiber): Node[] {
2185
3039
  }
2186
3040
 
2187
3041
  function hasCommittedHostNode(fiber: Fiber): boolean {
3042
+ if (fiber.tag === "reactive-dom-block") {
3043
+ return getReactiveDomBlockNode(fiber.stateNode) !== undefined;
3044
+ }
3045
+
2188
3046
  if (
2189
3047
  (fiber.tag === "host-component" || fiber.tag === "host-text") &&
2190
3048
  fiber.stateNode instanceof Node
@@ -2249,6 +3107,13 @@ function commitHostFiber(
2249
3107
  return [text];
2250
3108
  }
2251
3109
 
3110
+ if (fiber.tag === "reactive-dom-block") {
3111
+ const node = getReactiveDomBlockNode(fiber.stateNode);
3112
+ fiber.memoizedProps = fiber.pendingProps;
3113
+ finishCommittedFiber(fiber);
3114
+ return node === undefined ? [] : [node];
3115
+ }
3116
+
2252
3117
  if (fiber.tag === "host-component") {
2253
3118
  const element = fiber.stateNode;
2254
3119
 
@@ -2269,6 +3134,7 @@ function commitHostFiber(
2269
3134
  return [element];
2270
3135
  }
2271
3136
 
3137
+ const isDomElement = isDomHostElement(element);
2272
3138
  const props = fiber.pendingProps as Record<string, unknown>;
2273
3139
  const previousProps = fiber.memoizedProps as Record<string, unknown> | undefined;
2274
3140
  const directTextChild =
@@ -2294,7 +3160,7 @@ function commitHostFiber(
2294
3160
  fiber.hydrateExisting !== true &&
2295
3161
  isRowTextOnlyUpdate(fiber.memoizedProps, props);
2296
3162
 
2297
- if (isDomHostElement(element) && !propsAreUnchanged && !propsAreChildrenOnly && !textOnlyRowUpdate) {
3163
+ if (isDomElement && !propsAreUnchanged && !propsAreChildrenOnly && !textOnlyRowUpdate) {
2298
3164
  applyProps(element, props, path, {
2299
3165
  ...options,
2300
3166
  eventRoot,
@@ -2313,8 +3179,8 @@ function commitHostFiber(
2313
3179
  ) {
2314
3180
  const childNodes = commitHostChildren(fiber.child, element, eventRoot, `${path}.c`, options);
2315
3181
  if (
2316
- !(isDomHostElement(element) && childNodes.length === 0 && committedPortalContainers.has(element)) &&
2317
- !(isDomHostElement(element) && shouldPreserveContentEditableChildren(element, props, childNodes))
3182
+ !(isDomElement && childNodes.length === 0 && committedPortalContainers.has(element)) &&
3183
+ !(isDomElement && shouldPreserveContentEditableChildren(element, props, childNodes))
2318
3184
  ) {
2319
3185
  syncChildNodes(element as ParentNode, childNodes);
2320
3186
  }
@@ -2322,7 +3188,7 @@ function commitHostFiber(
2322
3188
  commitHostChildren(fiber.child, element, eventRoot, `${path}.c`, options);
2323
3189
  }
2324
3190
 
2325
- if (isDomHostElement(element)) {
3191
+ if (isDomElement && isFormHostType(fiber.type)) {
2326
3192
  applyPostChildFormProps(element, props, previousProps);
2327
3193
  }
2328
3194
  applyChangedRef(previousProps?.ref, props.ref, element);
@@ -2432,7 +3298,7 @@ function commitHostFiber(
2432
3298
  : container instanceof Element
2433
3299
  ? container
2434
3300
  : eventRoot;
2435
- const portalOptions = withPortalDocumentRef(options, container);
3301
+ const portalOptions = withPortalHostOptions(options, container);
2436
3302
  const childNodes = commitHostChildren(
2437
3303
  fiber.child,
2438
3304
  container as ParentNode,
@@ -2460,6 +3326,36 @@ function finishCommittedFiber(fiber: Fiber): void {
2460
3326
  fiber.hostChildListChanged = false;
2461
3327
  }
2462
3328
 
3329
+ function getReactiveDomBlockNode(state: unknown): ChildNode | undefined {
3330
+ if (
3331
+ typeof state === "object" &&
3332
+ state !== null &&
3333
+ "node" in state &&
3334
+ (state as { node?: unknown }).node instanceof Node
3335
+ ) {
3336
+ return (state as { node: ChildNode }).node;
3337
+ }
3338
+
3339
+ return undefined;
3340
+ }
3341
+
3342
+ function disposeReactiveDomBlockState(
3343
+ state: unknown,
3344
+ seen: Set<unknown>,
3345
+ ): void {
3346
+ if (typeof state !== "object" || state === null || seen.has(state)) {
3347
+ return;
3348
+ }
3349
+
3350
+ seen.add(state);
3351
+ const dispose = (state as { dispose?: unknown }).dispose;
3352
+
3353
+ if (typeof dispose === "function") {
3354
+ dispose();
3355
+ (state as { dispose?: unknown }).dispose = undefined;
3356
+ }
3357
+ }
3358
+
2463
3359
  function hasChildListMutation(fiber: Fiber): boolean {
2464
3360
  return fiber.childListChanged || fiber.subtreeChildListChanged;
2465
3361
  }
@@ -2659,6 +3555,13 @@ function getDirectHostTextChild(children: unknown): string | undefined {
2659
3555
  : undefined;
2660
3556
  }
2661
3557
 
3558
+ // Only these host tags carry post-child form value/checked semantics. Gating on
3559
+ // the (cheap, already-known) type string lets the commit skip applyPostChildFormProps
3560
+ // and its instanceof probes for every other element.
3561
+ function isFormHostType(type: unknown): boolean {
3562
+ return type === "input" || type === "textarea" || type === "select";
3563
+ }
3564
+
2662
3565
  // This package has no Node type dependency; declare the minimal process
2663
3566
  // shape needed for the literal process.env.NODE_ENV expression below.
2664
3567
  declare const process: { env: Record<string, string | undefined> };
@@ -3138,13 +4041,26 @@ function tryReuseMemoBailout(
3138
4041
  return undefined;
3139
4042
  }
3140
4043
 
3141
- const fiber = getMemoBailoutFiber(
3142
- runtime,
3143
- current,
3144
- node.props,
3145
- previousMemoState,
3146
- canReuseCurrentFiber,
3147
- );
4044
+ if (memoStateNeedsActiveInstanceMark(previousMemoState)) {
4045
+ markActiveInstanceKeys(runtime, previousMemoState.instanceKeys);
4046
+ }
4047
+
4048
+ const fiber =
4049
+ canReuseCurrentFiber &&
4050
+ current.hasRefSubtree !== true &&
4051
+ current.hasDisposableResources !== true &&
4052
+ current.hydrateExisting !== true
4053
+ ? current
4054
+ : createWorkInProgress(current, node.props);
4055
+
4056
+ if (fiber === current) {
4057
+ current.pendingProps = node.props;
4058
+ current.flags = NoFlags;
4059
+ current.subtreeFlags = NoFlags;
4060
+ current.childListChanged = false;
4061
+ current.subtreeChildListChanged = false;
4062
+ current.hostChildListChanged = false;
4063
+ }
3148
4064
  fiber.type = node.type;
3149
4065
  fiber.child = getSkippedChild(current);
3150
4066
  fiber.memoizedState = previousMemoState;
@@ -3336,7 +4252,11 @@ function createPortalFiber(
3336
4252
  portal.children,
3337
4253
  runtime,
3338
4254
  `${path}.portal`,
3339
- { ...options, documentRef: portal.container.ownerDocument },
4255
+ {
4256
+ ...options,
4257
+ documentRef: portal.container.ownerDocument,
4258
+ namespace: namespaceForPortalContainer(portal.container),
4259
+ },
3340
4260
  );
3341
4261
  fiber.child = childResult.fiber;
3342
4262
  fiber.return = parent;
@@ -3511,6 +4431,35 @@ function collectInstanceKeys(runtime: RootRuntime, prefix: string): string[] {
3511
4431
  return collectRuntimeInstanceKeys(runtime, prefix);
3512
4432
  }
3513
4433
 
4434
+ function collectMemoInstanceKeys(runtime: RootRuntime, prefix: string): string[] {
4435
+ return readDependencyFreeMemoInstanceKey(runtime, prefix) === undefined
4436
+ ? collectInstanceKeys(runtime, prefix)
4437
+ : emptyInstanceKeys;
4438
+ }
4439
+
4440
+ function readDependencyFreeMemoInstanceKey(
4441
+ runtime: RootRuntime,
4442
+ prefix: string,
4443
+ ): string | undefined {
4444
+ const keys = runtime.instanceKeysByPrefix.get(prefix);
4445
+
4446
+ if (keys === undefined || keys.size !== 1 || !keys.has(prefix)) {
4447
+ return undefined;
4448
+ }
4449
+
4450
+ const instance = runtime.instances.get(prefix) as RuntimeInstanceLike | undefined;
4451
+
4452
+ if (
4453
+ instance === undefined ||
4454
+ instance.contextDependencies !== undefined ||
4455
+ (instance.hooks !== undefined && instance.hooks.length > 0)
4456
+ ) {
4457
+ return undefined;
4458
+ }
4459
+
4460
+ return prefix;
4461
+ }
4462
+
3514
4463
  function markActiveInstanceKeys(runtime: RootRuntime, keys: readonly string[]): void {
3515
4464
  for (const key of keys) {
3516
4465
  runtime.activeInstanceKeys?.add(key);
@@ -3550,6 +4499,10 @@ function getMemoBailoutFiber(
3550
4499
  state: MemoFiberState,
3551
4500
  canReuseCurrentFiber: boolean,
3552
4501
  ): Fiber {
4502
+ if (memoStateNeedsActiveInstanceMark(state)) {
4503
+ markActiveInstanceKeys(runtime, state.instanceKeys);
4504
+ }
4505
+
3553
4506
  if (canReuseCurrentFiber && canReuseMemoBailoutFiber(current, state)) {
3554
4507
  current.pendingProps = pendingProps;
3555
4508
  current.flags = NoFlags;
@@ -3561,9 +4514,6 @@ function getMemoBailoutFiber(
3561
4514
  }
3562
4515
 
3563
4516
  const fiber = createWorkInProgress(current, pendingProps);
3564
- if (memoStateNeedsActiveInstanceMark(state)) {
3565
- markActiveInstanceKeys(runtime, state.instanceKeys);
3566
- }
3567
4517
  return fiber;
3568
4518
  }
3569
4519
 
@@ -3571,6 +4521,7 @@ function canReuseMemoBailoutFiber(current: Fiber, state: MemoFiberState): boolea
3571
4521
  return (
3572
4522
  state.hasRetainedInstanceDependencies === false &&
3573
4523
  current.hasRefSubtree !== true &&
4524
+ current.hasDisposableResources !== true &&
3574
4525
  current.hydrateExisting !== true
3575
4526
  );
3576
4527
  }
@@ -3782,10 +4733,11 @@ function isPortalHostContainer(value: unknown): value is ParentNode {
3782
4733
  );
3783
4734
  }
3784
4735
 
3785
- function withPortalDocumentRef(
4736
+ function withPortalHostOptions(
3786
4737
  options: RenderOptions,
3787
4738
  container: ParentNode,
3788
- ): RenderOptions & { documentRef?: Document | CustomHostDocument } {
4739
+ ): RenderOptions & { documentRef?: Document | CustomHostDocument; namespace: HostNamespace } {
4740
+ const namespace = namespaceForPortalContainer(container);
3789
4741
  const ownerDocument = (container as { ownerDocument?: unknown }).ownerDocument;
3790
4742
  if (
3791
4743
  typeof ownerDocument === "object" &&
@@ -3795,10 +4747,19 @@ function withPortalDocumentRef(
3795
4747
  return {
3796
4748
  ...options,
3797
4749
  documentRef: ownerDocument as Document | CustomHostDocument,
4750
+ namespace,
3798
4751
  };
3799
4752
  }
3800
4753
 
3801
- return options;
4754
+ return { ...options, namespace };
4755
+ }
4756
+
4757
+ function namespaceForPortalContainer(container: ParentNode): HostNamespace {
4758
+ if (container instanceof SVGElement && container.localName !== "foreignObject") {
4759
+ return "svg";
4760
+ }
4761
+
4762
+ return "html";
3802
4763
  }
3803
4764
 
3804
4765
  function committedHostNodesFromState(state: unknown): Node[] {