@reckona/mreact-compat 0.0.170 → 0.0.172

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 +7 -0
  23. package/dist/hooks.d.ts.map +1 -1
  24. package/dist/hooks.js +122 -41
  25. package/dist/hooks.js.map +1 -1
  26. package/dist/host-reconciler.d.ts +1 -0
  27. package/dist/host-reconciler.d.ts.map +1 -1
  28. package/dist/host-reconciler.js +659 -50
  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 +3 -1
  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 +1 -0
  52. package/src/fiber.ts +4 -0
  53. package/src/hooks.ts +173 -41
  54. package/src/host-reconciler.ts +979 -64
  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 +3 -0
@@ -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,42 @@ 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
+ function disposeHostFiberChildResources(
374
+ fiber: Fiber | undefined,
375
+ seen: Set<unknown>,
376
+ ): void {
377
+ let cursor = fiber;
378
+
379
+ while (cursor !== undefined) {
380
+ if (cursor.hasDisposableResources === true) {
381
+ if (cursor.tag === "reactive-dom-block") {
382
+ disposeReactiveDomBlockState(cursor.stateNode, seen);
383
+ }
384
+
385
+ disposeHostFiberChildResources(cursor.child, seen);
386
+ }
387
+
388
+ cursor = cursor.sibling;
389
+ }
390
+ }
391
+
320
392
  function reconcileHostChild(
321
393
  parent: Fiber,
322
394
  currentFirstChild: Fiber | undefined,
@@ -342,7 +414,15 @@ function reconcileHostChild(
342
414
  const rowResult =
343
415
  children === undefined
344
416
  ? undefined
345
- : reconcileKeyedRowHostChildren(parent, currentFirstChild, children, options);
417
+ : reconcileKeyedRowHostChildren(parent, currentFirstChild, children, options) ??
418
+ reconcileKeyedMemoRowHostChildren(
419
+ parent,
420
+ currentFirstChild,
421
+ children,
422
+ runtime,
423
+ path,
424
+ options,
425
+ );
346
426
  if (rowResult !== undefined) {
347
427
  return rowResult;
348
428
  }
@@ -359,6 +439,7 @@ function reconcileHostChild(
359
439
  let skipRemainingKeyedLookup = false;
360
440
  let sequentialCurrentCursor = currentFirstChild;
361
441
  let childListOrderChanged = false;
442
+ let dirtyChildren: Fiber[] | undefined;
362
443
  let usedCurrentChildren =
363
444
  currentFirstChild === undefined || hasKeyedChildren ? undefined : new Set<Fiber>();
364
445
  const ensureUsedCurrentChildren = (): Set<Fiber> => {
@@ -404,11 +485,28 @@ function reconcileHostChild(
404
485
  currentKeyed?.sibling?.key === key &&
405
486
  canSkipSingleDeletedKeyedFiber(children, index, currentKeyed.sibling)
406
487
  ) {
488
+ const deleted = currentKeyed;
489
+ const matched = currentKeyed.sibling;
490
+ const suffixResult = tryReuseDependencyFreeMemoRemovalSuffix(
491
+ parent,
492
+ children,
493
+ index,
494
+ deleted,
495
+ matched,
496
+ runtime,
497
+ options,
498
+ first,
499
+ previous,
500
+ consumed,
501
+ );
502
+ if (suffixResult !== undefined) {
503
+ return suffixResult;
504
+ }
407
505
  childListOrderChanged = true;
408
506
  ensureUsedCurrentChildren();
409
- matchedCurrent = currentKeyed.sibling;
507
+ matchedCurrent = matched;
410
508
  canReuseMatchedCurrentFiber = false;
411
- currentKeyed = currentKeyed.sibling.sibling;
509
+ currentKeyed = matched.sibling;
412
510
  } else {
413
511
  if (
414
512
  children !== undefined &&
@@ -489,6 +587,14 @@ function reconcileHostChild(
489
587
  && fiber.memoizedState === undefined
490
588
  ) {
491
589
  fiber.memoizedState = index;
590
+ } else if (
591
+ fiber.tag === "memo" &&
592
+ (fiber.stateNode === undefined || typeof fiber.stateNode === "number")
593
+ ) {
594
+ fiber.stateNode = index;
595
+ }
596
+ if (hasHostCommitWork(fiber)) {
597
+ (dirtyChildren ??= []).push(fiber);
492
598
  }
493
599
  previous = fiber;
494
600
  }
@@ -500,6 +606,7 @@ function reconcileHostChild(
500
606
  }
501
607
  parent.childListChanged =
502
608
  childListOrderChanged || childFiberListShapeChanged(currentFirstChild, first);
609
+ recordDirtyChildCommitHints(parent, dirtyChildren);
503
610
 
504
611
  return { fiber: first, consumed };
505
612
  }
@@ -528,6 +635,7 @@ function reconcileKeyedRowHostChildren(
528
635
  let subtreeFlags = NoFlags;
529
636
  let subtreeChildListChanged = false;
530
637
  let hasRefSubtree = false;
638
+ let hasDisposableResources = false;
531
639
  let appendSuffix: AppendSuffixCommitHint | undefined;
532
640
  const canReuseUnchangedRows = hasSameKeyOrderPrefix(currentFirstChild, children);
533
641
  const row = createKeyedRowHostElementScratch();
@@ -593,6 +701,9 @@ function reconcileKeyedRowHostChildren(
593
701
  if (fiber.hasRefSubtree) {
594
702
  hasRefSubtree = true;
595
703
  }
704
+ if (fiber.hasDisposableResources) {
705
+ hasDisposableResources = true;
706
+ }
596
707
  subtreeFlags |= fiber.flags | fiber.subtreeFlags;
597
708
  subtreeChildListChanged =
598
709
  subtreeChildListChanged ||
@@ -610,6 +721,7 @@ function reconcileKeyedRowHostChildren(
610
721
  }
611
722
 
612
723
  parent.hasRefSubtree = hasRefSubtree;
724
+ parent.hasDisposableResources = hasDisposableResources;
613
725
  parent.subtreeFlags = subtreeFlags;
614
726
  parent.subtreeChildListChanged = subtreeChildListChanged;
615
727
  parent.childListChanged = listShapeChanged;
@@ -619,6 +731,382 @@ function reconcileKeyedRowHostChildren(
619
731
  return { fiber: first, consumed: 0 };
620
732
  }
621
733
 
734
+ function isKeyedMemoRowCandidate(node: ReactCompatNode): boolean {
735
+ return (
736
+ isReactCompatElement(node) &&
737
+ node.key !== null &&
738
+ node.ref === null &&
739
+ isMemoType(node.type)
740
+ );
741
+ }
742
+
743
+ // Marker the compiler stamps on a lowered host-only component that returns its
744
+ // props verbatim as a reactive block (`createReactiveDomBlock(render, props)`).
745
+ // Such a component is pure and structurally static, so on a props change the
746
+ // reconciler can drive its committed block straight through the prop cell instead
747
+ // of re-invoking the component (the block's bound DOM updates via subscriptions).
748
+ const STATIC_REACTIVE_BLOCK_MARKER = "__mreactStaticBlock";
749
+
750
+ function isStaticReactiveBlockComponent(type: unknown): boolean {
751
+ return (
752
+ typeof type === "function" &&
753
+ (type as unknown as Record<string, unknown>)[STATIC_REACTIVE_BLOCK_MARKER] === true
754
+ );
755
+ }
756
+
757
+ // In-place bailout for a keyed memo row whose props are unchanged and which has
758
+ // no hook/context/effect dependencies. The caller has proven the key order is
759
+ // unchanged (hasSameKeyOrderPrefix), so the fiber keeps its position and can be
760
+ // reused WITHOUT createWorkInProgress — no allocation, and the retained subtree
761
+ // (including any reactive-dom-block subscriptions) is left untouched. Returns
762
+ // undefined when the row must re-render or has dependencies (handled by the
763
+ // general per-child path instead). Mirrors tryReuseDependencyFreeMemoBailout +
764
+ // getMemoBailoutFiber's in-place branch, but is safe to reuse a fiber with
765
+ // disposable resources because a stable position means nothing is disposed.
766
+ function tryReuseDependencyFreeKeyedMemoRow(
767
+ matched: Fiber,
768
+ child: ReactCompatElement,
769
+ ): Fiber | undefined {
770
+ if (
771
+ matched.tag !== "memo" ||
772
+ matched.type !== child.type ||
773
+ matched.hasRefSubtree === true ||
774
+ matched.hydrateExisting === true
775
+ ) {
776
+ return undefined;
777
+ }
778
+
779
+ const state = matched.memoizedState as MemoFiberState | undefined;
780
+
781
+ if (
782
+ state === undefined ||
783
+ state.hasDirtyInstanceDependencies !== false ||
784
+ state.hasUnflushedEffectDependencies !== false ||
785
+ state.hasRetainedInstanceDependencies !== false ||
786
+ !areMemoPropsEqual(
787
+ child.type as { compare?: (a: Record<string, unknown>, b: Record<string, unknown>) => boolean },
788
+ state.props,
789
+ child.props as Record<string, unknown>,
790
+ )
791
+ ) {
792
+ return undefined;
793
+ }
794
+
795
+ matched.pendingProps = child.props;
796
+ matched.flags = NoFlags;
797
+ matched.subtreeFlags = NoFlags;
798
+ matched.childListChanged = false;
799
+ matched.subtreeChildListChanged = false;
800
+ matched.hostChildListChanged = false;
801
+ matched.child = getSkippedChild(matched);
802
+ return matched;
803
+ }
804
+
805
+ // In-place CHANGED-row update for a keyed memo row whose inner component is a
806
+ // compiler-marked static reactive block (returns its props verbatim as the block
807
+ // props). Instead of re-invoking the component to rebuild a throwaway block
808
+ // element, push the new props straight into the committed block's prop cell — the
809
+ // bound DOM updates via subscriptions, exactly as the normal block re-render path
810
+ // would, but without the memo/component render machinery. Same-order is proven by
811
+ // the caller, so the fiber is reused in place. Returns undefined when this is not
812
+ // a marked, dependency-free, single-block memo row (the general path handles it).
813
+ function tryCellUpdateStaticBlockMemoRow(
814
+ matched: Fiber,
815
+ child: ReactCompatElement,
816
+ ): Fiber | undefined {
817
+ if (
818
+ matched.tag !== "memo" ||
819
+ matched.type !== child.type ||
820
+ matched.hasRefSubtree === true ||
821
+ matched.hydrateExisting === true ||
822
+ !isStaticReactiveBlockComponent((matched.type as { type?: unknown }).type)
823
+ ) {
824
+ return undefined;
825
+ }
826
+
827
+ const state = matched.memoizedState as MemoFiberState | undefined;
828
+
829
+ if (
830
+ state === undefined ||
831
+ state.hasDirtyInstanceDependencies !== false ||
832
+ state.hasUnflushedEffectDependencies !== false ||
833
+ state.hasRetainedInstanceDependencies !== false
834
+ ) {
835
+ return undefined;
836
+ }
837
+
838
+ // Navigate memo -> component fiber -> reactive-dom-block fiber. The marked
839
+ // component renders exactly one static block, so anything else is unexpected.
840
+ const componentFiber = matched.child;
841
+ if (componentFiber === undefined || componentFiber.sibling !== undefined) {
842
+ return undefined;
843
+ }
844
+
845
+ const blockFiber = componentFiber.child;
846
+ if (
847
+ blockFiber === undefined ||
848
+ blockFiber.tag !== "reactive-dom-block" ||
849
+ blockFiber.sibling !== undefined
850
+ ) {
851
+ return undefined;
852
+ }
853
+
854
+ const blockState = blockFiber.stateNode as ReactiveDomBlockState | undefined;
855
+ if (blockState?.propCell === undefined) {
856
+ return undefined;
857
+ }
858
+
859
+ // Drive the bound DOM through the prop cell (the marker guarantees the block's
860
+ // props are the component's props verbatim), then reuse the memo fiber in place.
861
+ setReactivePropCell(blockState.propCell, child.props as Record<string, unknown>);
862
+ matched.memoizedState = { ...state, props: child.props as Record<string, unknown> };
863
+ matched.pendingProps = child.props;
864
+ matched.flags = NoFlags;
865
+ matched.subtreeFlags = NoFlags;
866
+ matched.childListChanged = false;
867
+ matched.subtreeChildListChanged = false;
868
+ matched.hostChildListChanged = false;
869
+ matched.child = getSkippedChild(matched);
870
+ return matched;
871
+ }
872
+
873
+ // Fast path for a keyed list of memo-wrapped rows (e.g. `<RowMemo key={id} />`)
874
+ // whose key order is UNCHANGED between renders — the js-framework-benchmark
875
+ // "select row" and "partial update" shapes. Walks current fibers and new
876
+ // children in lockstep: unchanged rows bail in place (no allocation, no general
877
+ // keyed-reconcile machinery), changed rows re-render through the proven
878
+ // per-child path. Any reorder / insert / delete / non-memo child / length change
879
+ // makes it return undefined so the general reconcile takes over.
880
+ function reconcileKeyedMemoRowHostChildren(
881
+ parent: Fiber,
882
+ currentFirstChild: Fiber | undefined,
883
+ children: readonly ReactCompatNode[],
884
+ runtime: RootRuntime | undefined,
885
+ path: string,
886
+ options: FiberHydrationOptions,
887
+ ): FiberReconcileResult | undefined {
888
+ if (
889
+ children.length === 0 ||
890
+ currentFirstChild === undefined ||
891
+ runtime === undefined ||
892
+ options.previousNodes !== undefined ||
893
+ !isKeyedMemoRowCandidate(children[0]) ||
894
+ !hasSameKeyOrderPrefix(currentFirstChild, children)
895
+ ) {
896
+ return undefined;
897
+ }
898
+
899
+ let currentKeyed: Fiber | undefined = currentFirstChild;
900
+ let first: Fiber | undefined;
901
+ let previous: Fiber | undefined;
902
+ let subtreeFlags = NoFlags;
903
+ let subtreeChildListChanged = false;
904
+ let hasRefSubtree = false;
905
+ let hasDisposableResources = false;
906
+ let dirtyChildren: Fiber[] | undefined;
907
+
908
+ for (let index = 0; index < children.length; index += 1) {
909
+ const child = children[index];
910
+
911
+ if (currentKeyed === undefined || !isKeyedMemoRowCandidate(child)) {
912
+ return undefined;
913
+ }
914
+
915
+ const matched = currentKeyed;
916
+ currentKeyed = currentKeyed.sibling;
917
+ const childElement = child as ReactCompatElement;
918
+
919
+ let fiber =
920
+ tryReuseDependencyFreeKeyedMemoRow(matched, childElement) ??
921
+ tryCellUpdateStaticBlockMemoRow(matched, childElement);
922
+ if (fiber === undefined) {
923
+ const result = createHostFiber(
924
+ parent,
925
+ matched,
926
+ child,
927
+ childElement.key ?? undefined,
928
+ runtime,
929
+ getReconcileChildPath(path, child, index, options),
930
+ options,
931
+ false,
932
+ );
933
+ fiber = result.fiber;
934
+
935
+ if (fiber === undefined) {
936
+ return undefined;
937
+ }
938
+
939
+ if (fiber !== matched && fiber.alternate !== matched) {
940
+ markOptimizedChildForDeletion(parent, matched);
941
+ }
942
+ }
943
+
944
+ if (first === undefined) {
945
+ first = fiber;
946
+ } else if (previous !== undefined) {
947
+ previous.sibling = fiber;
948
+ }
949
+
950
+ fiber.return = parent;
951
+ fiber.sibling = undefined;
952
+ if (fiber.hasRefSubtree) {
953
+ hasRefSubtree = true;
954
+ }
955
+ if (fiber.hasDisposableResources) {
956
+ hasDisposableResources = true;
957
+ }
958
+ subtreeFlags |= fiber.flags | fiber.subtreeFlags;
959
+ subtreeChildListChanged =
960
+ subtreeChildListChanged || fiber.childListChanged || fiber.subtreeChildListChanged;
961
+ if (
962
+ fiber.tag === "memo" &&
963
+ (fiber.stateNode === undefined || typeof fiber.stateNode === "number")
964
+ ) {
965
+ fiber.stateNode = index;
966
+ }
967
+ if (hasHostCommitWork(fiber)) {
968
+ (dirtyChildren ??= []).push(fiber);
969
+ }
970
+ previous = fiber;
971
+ }
972
+
973
+ if (currentKeyed !== undefined) {
974
+ return undefined;
975
+ }
976
+
977
+ parent.hasRefSubtree = hasRefSubtree;
978
+ parent.hasDisposableResources = hasDisposableResources;
979
+ parent.subtreeFlags = subtreeFlags;
980
+ parent.subtreeChildListChanged = subtreeChildListChanged;
981
+ parent.childListChanged = false;
982
+ recordDirtyChildCommitHints(parent, dirtyChildren);
983
+ return { fiber: first, consumed: 0 };
984
+ }
985
+
986
+ function canReuseDependencyFreeMemoAtKey(
987
+ current: Fiber | undefined,
988
+ node: ReactCompatElement,
989
+ key: string,
990
+ ): boolean {
991
+ return (
992
+ current !== undefined &&
993
+ current.key === key &&
994
+ current.tag === "memo" &&
995
+ current.type === node.type &&
996
+ current.hasRefSubtree !== true &&
997
+ current.hasDisposableResources !== true &&
998
+ current.hydrateExisting !== true &&
999
+ isMemoType(node.type)
1000
+ );
1001
+ }
1002
+
1003
+ function tryReuseDependencyFreeMemoRemovalSuffix(
1004
+ parent: Fiber,
1005
+ children: readonly ReactCompatNode[],
1006
+ startIndex: number,
1007
+ removed: Fiber,
1008
+ matchedCurrent: Fiber,
1009
+ runtime: RootRuntime | undefined,
1010
+ options: FiberHydrationOptions,
1011
+ prefixFirst: Fiber | undefined,
1012
+ prefixPrevious: Fiber | undefined,
1013
+ consumed: number,
1014
+ ): FiberReconcileResult | undefined {
1015
+ if (
1016
+ runtime === undefined ||
1017
+ options.previousNodes !== undefined ||
1018
+ removed.hasRefSubtree
1019
+ ) {
1020
+ return undefined;
1021
+ }
1022
+
1023
+ const suffix: Fiber[] = [];
1024
+ let current: Fiber | undefined = matchedCurrent;
1025
+
1026
+ for (let index = startIndex; index < children.length; index += 1) {
1027
+ const child = children[index];
1028
+ const key = getNodeKey(child);
1029
+
1030
+ if (key === undefined || !isReactCompatElement(child) || !isMemoType(child.type)) {
1031
+ return undefined;
1032
+ }
1033
+
1034
+ if (!canReuseDependencyFreeMemoAtKey(current, child, key)) {
1035
+ return undefined;
1036
+ }
1037
+
1038
+ if (current === undefined) {
1039
+ return undefined;
1040
+ }
1041
+
1042
+ const matched: Fiber = current;
1043
+ const fiber = reuseDependencyFreeMemoFiber(matched, child);
1044
+
1045
+ if (fiber === undefined) {
1046
+ return undefined;
1047
+ }
1048
+
1049
+ suffix.push(fiber);
1050
+ current = matched.sibling;
1051
+ }
1052
+
1053
+ if (current !== undefined) {
1054
+ return undefined;
1055
+ }
1056
+
1057
+ let first = prefixFirst;
1058
+ let previous = prefixPrevious;
1059
+
1060
+ for (const fiber of suffix) {
1061
+ if (first === undefined) {
1062
+ first = fiber;
1063
+ } else if (previous !== undefined) {
1064
+ previous.sibling = fiber;
1065
+ }
1066
+
1067
+ fiber.return = parent;
1068
+ fiber.sibling = undefined;
1069
+ previous = fiber;
1070
+ }
1071
+
1072
+ parent.childListChanged = true;
1073
+ parent.deletions = [removed];
1074
+ markOptimizedChildForDeletion(parent, removed);
1075
+ return { fiber: first, consumed };
1076
+ }
1077
+
1078
+ function reuseDependencyFreeMemoFiber(
1079
+ current: Fiber,
1080
+ node: ReactCompatElement,
1081
+ ): Fiber | undefined {
1082
+ if (!isMemoType(node.type)) {
1083
+ return undefined;
1084
+ }
1085
+
1086
+ const previousMemoState = current.memoizedState as MemoFiberState | undefined;
1087
+
1088
+ if (
1089
+ previousMemoState === undefined ||
1090
+ previousMemoState.hasDirtyInstanceDependencies !== false ||
1091
+ previousMemoState.hasUnflushedEffectDependencies !== false ||
1092
+ previousMemoState.hasRetainedInstanceDependencies !== false ||
1093
+ !areMemoPropsEqual(node.type, previousMemoState.props, node.props)
1094
+ ) {
1095
+ return undefined;
1096
+ }
1097
+
1098
+ current.pendingProps = node.props;
1099
+ current.flags = NoFlags;
1100
+ current.subtreeFlags = NoFlags;
1101
+ current.childListChanged = false;
1102
+ current.subtreeChildListChanged = false;
1103
+ current.hostChildListChanged = false;
1104
+ current.type = node.type;
1105
+ current.child = getSkippedChild(current);
1106
+ current.memoizedState = previousMemoState;
1107
+ return current;
1108
+ }
1109
+
622
1110
  function canStoreAppendSuffixCommitHint(parent: Fiber): boolean {
623
1111
  return (
624
1112
  parent.tag === "fragment" ||
@@ -712,6 +1200,7 @@ function getReusableKeyedRowHostFiber(
712
1200
  current.subtreeChildListChanged = false;
713
1201
  current.hostChildListChanged = false;
714
1202
  current.hasRefSubtree = false;
1203
+ current.hasDisposableResources = false;
715
1204
  return current;
716
1205
  }
717
1206
 
@@ -793,6 +1282,7 @@ function createKeyedRowHostFiber(
793
1282
  fiber.pendingProps = node.props;
794
1283
  fiber.hostChildListChanged = false;
795
1284
  fiber.hasRefSubtree = false;
1285
+ fiber.hasDisposableResources = false;
796
1286
 
797
1287
  if (current === undefined || fiber.alternate !== current) {
798
1288
  fiber.flags |= Placement;
@@ -956,6 +1446,9 @@ function bubbleHostChild(parent: Fiber, child: Fiber): void {
956
1446
  if (child.hasRefSubtree) {
957
1447
  parent.hasRefSubtree = true;
958
1448
  }
1449
+ if (child.hasDisposableResources) {
1450
+ parent.hasDisposableResources = true;
1451
+ }
959
1452
  parent.subtreeFlags |= child.flags | child.subtreeFlags;
960
1453
  parent.subtreeChildListChanged =
961
1454
  parent.subtreeChildListChanged ||
@@ -963,8 +1456,21 @@ function bubbleHostChild(parent: Fiber, child: Fiber): void {
963
1456
  child.subtreeChildListChanged;
964
1457
  }
965
1458
 
1459
+ function recordDirtyChildCommitHints(
1460
+ parent: Fiber,
1461
+ dirtyChildren: Fiber[] | undefined,
1462
+ ): void {
1463
+ if (parent.childListChanged || dirtyChildren === undefined || dirtyChildren.length === 0) {
1464
+ return;
1465
+ }
1466
+
1467
+ // Reuse the effect-list slot only when there are no child-list deletions.
1468
+ parent.deletions = dirtyChildren;
1469
+ }
1470
+
966
1471
  function resetFiberRefSubtree(fiber: Fiber): void {
967
1472
  fiber.hasRefSubtree = false;
1473
+ fiber.hasDisposableResources = false;
968
1474
  }
969
1475
 
970
1476
  function includeNodeRef(fiber: Fiber, node: ReactCompatNode): void {
@@ -1142,6 +1648,13 @@ function createHostFiberImpl(
1142
1648
  return { fiber: undefined, consumed: 0 };
1143
1649
  }
1144
1650
 
1651
+ // Host elements (string type) are by far the most common node. Dispatch them
1652
+ // before the component-type checks below, none of which a string can match,
1653
+ // so each reconciled host element skips ~12 type comparisons / probes.
1654
+ if (typeof node.type === "string") {
1655
+ return createHostComponentFiber(parent, current, node, key, runtime, path, options);
1656
+ }
1657
+
1145
1658
  if (node.type === Fragment) {
1146
1659
  const fiber =
1147
1660
  current?.tag === "fragment"
@@ -1269,6 +1782,37 @@ function createHostFiberImpl(
1269
1782
  return { fiber, consumed: options.previousNodes?.length ?? 0 };
1270
1783
  }
1271
1784
 
1785
+ if (node.type === REACTIVE_DOM_BLOCK_TYPE) {
1786
+ const blockProps = (node.props as unknown as ReactiveDomBlockProps).blockProps;
1787
+ if (current?.tag === "reactive-dom-block") {
1788
+ // Re-render: reuse the committed DOM/subscriptions and push the new props
1789
+ // into the prop cell instead of re-running render(). Bound text/attributes
1790
+ // update via their reactive subscriptions; the subtree is never reconciled.
1791
+ const fiber = createWorkInProgress(current, node.props);
1792
+ fiber.type = node.type;
1793
+ fiber.hasDisposableResources = true;
1794
+ const previousState = current.stateNode as ReactiveDomBlockState | undefined;
1795
+ if (previousState?.propCell !== undefined && blockProps !== undefined) {
1796
+ setReactivePropCell(previousState.propCell, blockProps);
1797
+ }
1798
+ fiber.stateNode = previousState;
1799
+ return { fiber, consumed: options.previousNodes?.length ?? 0 };
1800
+ }
1801
+
1802
+ const fiber = createFiber("reactive-dom-block", node.props, key);
1803
+ fiber.type = node.type;
1804
+ fiber.hasDisposableResources = true;
1805
+ const render = (node.props as unknown as ReactiveDomBlockProps).render;
1806
+ if (blockProps !== undefined) {
1807
+ const propCell = createReactivePropCell(blockProps);
1808
+ const result = render(createReactivePropProxy(propCell));
1809
+ fiber.stateNode = { node: result.node, dispose: result.dispose, propCell };
1810
+ } else {
1811
+ fiber.stateNode = (render as () => ReactiveDomBlockResult)();
1812
+ }
1813
+ return { fiber, consumed: options.previousNodes?.length ?? 0 };
1814
+ }
1815
+
1272
1816
  if (isReactCompatProvider(node.type)) {
1273
1817
  const fiber =
1274
1818
  current?.tag === "context-provider" && current.type === node.type
@@ -1390,8 +1934,11 @@ function createHostFiberImpl(
1390
1934
  fiber.type = memoType;
1391
1935
 
1392
1936
  const renderedElement: ReactCompatElement = {
1393
- ...node,
1937
+ $$typeof: node.$$typeof,
1394
1938
  type: memoType.type,
1939
+ key: node.key,
1940
+ ref: node.ref,
1941
+ props: node.props,
1395
1942
  };
1396
1943
  const childResult = createHostFiber(
1397
1944
  fiber,
@@ -1408,7 +1955,7 @@ function createHostFiberImpl(
1408
1955
  fiber.child.sibling = undefined;
1409
1956
  bubbleHostChild(fiber, fiber.child);
1410
1957
  }
1411
- const instanceKeys = collectInstanceKeys(runtime, memoPath);
1958
+ const instanceKeys = collectMemoInstanceKeys(runtime, memoPath);
1412
1959
  const hasClassDescendant = hasClassComponentDescendant(fiber.child);
1413
1960
  fiber.memoizedState = {
1414
1961
  props: node.props as Record<string, unknown>,
@@ -1439,8 +1986,11 @@ function createHostFiberImpl(
1439
1986
 
1440
1987
  if (lazyType.status === "resolved" && lazyType.resolved !== undefined) {
1441
1988
  const renderedElement: ReactCompatElement = {
1442
- ...node,
1989
+ $$typeof: node.$$typeof,
1443
1990
  type: lazyType.resolved,
1991
+ key: node.key,
1992
+ ref: node.ref,
1993
+ props: node.props,
1444
1994
  };
1445
1995
  const childResult = createHostFiber(
1446
1996
  fiber,
@@ -1625,61 +2175,95 @@ function createHostFiberImpl(
1625
2175
  const instanceKeys = collectInstanceKeys(runtime, path);
1626
2176
  fiber.stateNode = {
1627
2177
  element: node,
1628
- props: { ...node.props },
2178
+ props: node.props as Record<string, unknown>,
1629
2179
  instanceKeys,
1630
2180
  hasContextDependencies: hasContextDependency(runtime, instanceKeys),
1631
2181
  } satisfies FunctionFiberState;
1632
2182
  return { fiber, consumed: childResult.consumed };
1633
2183
  }
1634
2184
 
2185
+ return { fiber: undefined, consumed: 0 };
2186
+ }
2187
+
2188
+ // The host-component reconcile, split out of createHostFiberImpl so host
2189
+ // elements can be dispatched before the component-type checks. Only called with
2190
+ // a string element type.
2191
+ function createHostComponentFiber(
2192
+ parent: Fiber,
2193
+ current: Fiber | undefined,
2194
+ node: ReactCompatElement,
2195
+ key: string | undefined,
2196
+ runtime: RootRuntime | undefined,
2197
+ path: string,
2198
+ options: FiberHydrationOptions,
2199
+ ): FiberReconcileResult {
1635
2200
  if (typeof node.type !== "string") {
1636
2201
  return { fiber: undefined, consumed: 0 };
1637
2202
  }
1638
2203
 
2204
+ const initialHostOnlyFiber = tryCreateInitialHostOnlyFiber(
2205
+ current,
2206
+ node,
2207
+ key,
2208
+ options,
2209
+ );
2210
+ if (initialHostOnlyFiber !== undefined) {
2211
+ return { fiber: initialHostOnlyFiber, consumed: 0 };
2212
+ }
2213
+
1639
2214
  const elementNamespace = namespaceForHostElement(options.namespace ?? "html", node.type);
1640
2215
  const childNamespace = namespaceForHostChildren(elementNamespace, node.type);
2216
+ const reusableCurrent =
2217
+ current?.tag === "host-component" && current.type === node.type ? current : undefined;
1641
2218
  const fiber =
1642
- current?.tag === "host-component" && current.type === node.type
1643
- ? createWorkInProgress(current, node.props)
2219
+ reusableCurrent !== undefined
2220
+ ? createWorkInProgress(reusableCurrent, node.props)
1644
2221
  : createFiber("host-component", node.props, key);
2222
+ // The hydration node bookkeeping only matters when hydrating. Skipping it for
2223
+ // the common (non-hydration) render avoids an isHostElement(undefined) probe
2224
+ // and the mismatch checks on every reconciled element.
1645
2225
  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
- }
2226
+ let existingElement: HostElement | undefined;
2227
+ let tagMatches = false;
2228
+ if (options.previousNodes !== undefined) {
2229
+ existingElement = isHostElement(existing) ? existing : undefined;
2230
+ tagMatches =
2231
+ existingElement !== undefined &&
2232
+ hostElementMatches(existingElement, node.type, elementNamespace);
2233
+
2234
+ if (existing === undefined) {
2235
+ reportMissingHydrationNode(options, path);
2236
+ } else if (!isHostElement(existing)) {
2237
+ reportHydrationNodeTypeMismatch(options, path, `<${node.type}>`, existing);
2238
+ }
1656
2239
 
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);
2240
+ if (existingElement !== undefined && !tagMatches) {
2241
+ reportRecoverable(
2242
+ options,
2243
+ "tag",
2244
+ path,
2245
+ new Error(
2246
+ `Hydration tag mismatch: expected <${node.type}> but found <${existingElement.tagName.toLowerCase()}>.`,
2247
+ ),
2248
+ );
2249
+ reportElementTextMismatch(options, `${path}.c`, existingElement, node.props.children);
2250
+ }
1667
2251
  }
1668
2252
 
1669
2253
  fiber.type = node.type;
2254
+ // When reusing a same-type current fiber, its stateNode was created for this
2255
+ // exact tag and namespace (same tree position), so the hostElementMatches
2256
+ // re-check (localName + namespaceURI reads) is redundant.
1670
2257
  fiber.stateNode =
1671
2258
  tagMatches
1672
2259
  ? 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
2260
+ : reusableCurrent !== undefined && isHostElement(reusableCurrent.stateNode)
2261
+ ? reusableCurrent.stateNode
1678
2262
  : createHostElement(getDocumentRef(options), node.type, options.namespace ?? "html");
1679
- fiber.hydrateExisting = tagMatches && options.previousNodes !== undefined;
2263
+ fiber.hydrateExisting = tagMatches;
1680
2264
  const previousChildNodes =
1681
2265
  tagMatches && existingElement !== undefined
1682
- ? Array.from(existingElement.childNodes)
2266
+ ? Array.from((existingElement as Element).childNodes)
1683
2267
  : undefined;
1684
2268
  const directTextChild =
1685
2269
  shouldUseDirectHostTextChild() && previousChildNodes === undefined
@@ -1713,11 +2297,7 @@ function createHostFiberImpl(
1713
2297
  node.props.children as ReactCompatNode,
1714
2298
  runtime,
1715
2299
  `${path}.c`,
1716
- {
1717
- ...options,
1718
- namespace: childNamespace,
1719
- ...(previousChildNodes === undefined ? {} : { previousNodes: previousChildNodes }),
1720
- },
2300
+ getHostChildFiberOptions(options, childNamespace, previousChildNodes),
1721
2301
  );
1722
2302
  fiber.child = childResult.fiber;
1723
2303
  if (previousChildNodes !== undefined) {
@@ -1737,6 +2317,185 @@ function isFunctionComponentType(value: unknown): value is (
1737
2317
  );
1738
2318
  }
1739
2319
 
2320
+ function tryCreateInitialHostOnlyFiber(
2321
+ current: Fiber | undefined,
2322
+ node: ReactCompatElement,
2323
+ key: string | undefined,
2324
+ options: FiberHydrationOptions,
2325
+ ): Fiber | undefined {
2326
+ if (
2327
+ current !== undefined ||
2328
+ options.previousNodes !== undefined ||
2329
+ !shouldUseDirectHostTextChild() ||
2330
+ typeof node.type !== "string" ||
2331
+ !canCreateInitialHostOnlyNode(node)
2332
+ ) {
2333
+ return undefined;
2334
+ }
2335
+
2336
+ return createInitialHostOnlyElementFiber(
2337
+ node,
2338
+ key,
2339
+ options.namespace ?? "html",
2340
+ getDocumentRef(options),
2341
+ );
2342
+ }
2343
+
2344
+ function createInitialHostOnlyElementFiber(
2345
+ node: ReactCompatElement,
2346
+ key: string | undefined,
2347
+ namespace: HostNamespace,
2348
+ documentRef: Document | CustomHostDocument,
2349
+ ): Fiber {
2350
+ const elementType = node.type as string;
2351
+ const elementNamespace = namespaceForHostElement(namespace, elementType);
2352
+ const childNamespace = namespaceForHostChildren(elementNamespace, elementType);
2353
+ const fiber = createFiber("host-component", node.props, key);
2354
+ fiber.type = elementType;
2355
+ fiber.stateNode = createHostElement(documentRef, elementType, namespace);
2356
+ fiber.flags |= Placement;
2357
+ fiber.hostChildListChanged = true;
2358
+
2359
+ if (getDirectHostTextChild(node.props.children) === undefined) {
2360
+ fiber.child = createInitialHostOnlyChildList(
2361
+ fiber,
2362
+ node.props.children as ReactCompatNode,
2363
+ childNamespace,
2364
+ documentRef,
2365
+ );
2366
+ }
2367
+
2368
+ return fiber;
2369
+ }
2370
+
2371
+ function createInitialHostOnlyChildList(
2372
+ parent: Fiber,
2373
+ children: ReactCompatNode,
2374
+ namespace: HostNamespace,
2375
+ documentRef: Document | CustomHostDocument,
2376
+ ): Fiber | undefined {
2377
+ const normalized = normalizeChildren(children);
2378
+ let first: Fiber | undefined;
2379
+ let previous: Fiber | undefined;
2380
+
2381
+ for (let index = 0; index < normalized.length; index += 1) {
2382
+ const child = normalized[index];
2383
+ const fiber = createInitialHostOnlyChildFiber(child, namespace, documentRef);
2384
+
2385
+ if (fiber === undefined) {
2386
+ continue;
2387
+ }
2388
+
2389
+ fiber.return = parent;
2390
+ fiber.memoizedState = index;
2391
+
2392
+ if (first === undefined) {
2393
+ first = fiber;
2394
+ } else if (previous !== undefined) {
2395
+ previous.sibling = fiber;
2396
+ }
2397
+
2398
+ bubbleHostChild(parent, fiber);
2399
+ previous = fiber;
2400
+ }
2401
+
2402
+ return first;
2403
+ }
2404
+
2405
+ function createInitialHostOnlyChildFiber(
2406
+ node: ReactCompatNode,
2407
+ namespace: HostNamespace,
2408
+ documentRef: Document | CustomHostDocument,
2409
+ ): Fiber | undefined {
2410
+ if (node === null || node === undefined || typeof node === "boolean") {
2411
+ return undefined;
2412
+ }
2413
+
2414
+ if (typeof node === "string" || typeof node === "number") {
2415
+ const fiber = createFiber("host-text", String(node));
2416
+ fiber.stateNode = createHostTextNode(documentRef);
2417
+ fiber.flags |= Placement;
2418
+ return fiber;
2419
+ }
2420
+
2421
+ if (Array.isArray(node)) {
2422
+ const fiber = createFiber("fragment", node);
2423
+ fiber.child = createInitialHostOnlyChildList(fiber, node, namespace, documentRef);
2424
+ return fiber.child === undefined ? undefined : fiber;
2425
+ }
2426
+
2427
+ if (!isReactCompatElement(node) || typeof node.type !== "string") {
2428
+ return undefined;
2429
+ }
2430
+
2431
+ return createInitialHostOnlyElementFiber(
2432
+ node,
2433
+ node.key === null ? undefined : node.key,
2434
+ namespace,
2435
+ documentRef,
2436
+ );
2437
+ }
2438
+
2439
+ function canCreateInitialHostOnlyNode(node: ReactCompatNode): boolean {
2440
+ if (
2441
+ node === null ||
2442
+ node === undefined ||
2443
+ typeof node === "boolean" ||
2444
+ typeof node === "string" ||
2445
+ typeof node === "number"
2446
+ ) {
2447
+ return true;
2448
+ }
2449
+
2450
+ if (Array.isArray(node)) {
2451
+ return node.every(canCreateInitialHostOnlyNode);
2452
+ }
2453
+
2454
+ if (
2455
+ !isReactCompatElement(node) ||
2456
+ typeof node.type !== "string" ||
2457
+ node.ref !== null ||
2458
+ hasInitialHostOnlyExcludedProps(node.props)
2459
+ ) {
2460
+ return false;
2461
+ }
2462
+
2463
+ return canCreateInitialHostOnlyNode(node.props.children as ReactCompatNode);
2464
+ }
2465
+
2466
+ function hasInitialHostOnlyExcludedProps(props: Record<string, unknown>): boolean {
2467
+ return (
2468
+ hasOwnProperty.call(props, REACTIVE_TEXT_BINDING_META) ||
2469
+ props.dangerouslySetInnerHTML !== undefined ||
2470
+ props.contentEditable === true ||
2471
+ props.suppressContentEditableWarning === true ||
2472
+ props.value !== undefined ||
2473
+ props.defaultValue !== undefined ||
2474
+ props.checked !== undefined ||
2475
+ props.defaultChecked !== undefined
2476
+ );
2477
+ }
2478
+
2479
+ function getHostChildFiberOptions(
2480
+ options: FiberHydrationOptions,
2481
+ namespace: HostNamespace,
2482
+ previousNodes: readonly Node[] | undefined,
2483
+ ): FiberHydrationOptions {
2484
+ const namespaceUnchanged =
2485
+ options.namespace === namespace ||
2486
+ (options.namespace === undefined && namespace === "html");
2487
+
2488
+ if (previousNodes === undefined && namespaceUnchanged) {
2489
+ return options;
2490
+ }
2491
+
2492
+ return {
2493
+ ...options,
2494
+ namespace,
2495
+ ...(previousNodes === undefined ? {} : { previousNodes }),
2496
+ };
2497
+ }
2498
+
1740
2499
  function commitHostChildren(
1741
2500
  fiber: Fiber | undefined,
1742
2501
  parent: ParentNode,
@@ -1778,6 +2537,49 @@ function commitHostDirtyChildren(
1778
2537
  }
1779
2538
  }
1780
2539
 
2540
+ function commitHostDirtyChildrenOf(
2541
+ owner: Fiber,
2542
+ fiber: Fiber | undefined,
2543
+ parent: ParentNode,
2544
+ eventRoot: Element,
2545
+ path: string,
2546
+ options: RenderOptions = {},
2547
+ ): void {
2548
+ const dirtyChildren = readDirtyChildCommitHints(owner);
2549
+
2550
+ if (dirtyChildren === undefined) {
2551
+ commitHostDirtyChildren(fiber, parent, eventRoot, path, options);
2552
+ return;
2553
+ }
2554
+
2555
+ for (let index = 0; index < dirtyChildren.length; index += 1) {
2556
+ const dirtyChild = dirtyChildren[index];
2557
+
2558
+ if (dirtyChild !== undefined && hasHostCommitWork(dirtyChild)) {
2559
+ commitHostDirtyFiber(
2560
+ dirtyChild,
2561
+ parent,
2562
+ eventRoot,
2563
+ joinCommitPath(path, String(getDirtyChildCommitIndex(dirtyChild, index))),
2564
+ options,
2565
+ );
2566
+ }
2567
+ }
2568
+ owner.deletions = undefined;
2569
+ }
2570
+
2571
+ function readDirtyChildCommitHints(fiber: Fiber): Fiber[] | undefined {
2572
+ return fiber.childListChanged ? undefined : fiber.deletions;
2573
+ }
2574
+
2575
+ function getDirtyChildCommitIndex(fiber: Fiber, fallback: number): number {
2576
+ if (typeof fiber.memoizedState === "number") {
2577
+ return fiber.memoizedState;
2578
+ }
2579
+
2580
+ return typeof fiber.stateNode === "number" ? fiber.stateNode : fallback;
2581
+ }
2582
+
1781
2583
  function commitHostDirtyFiber(
1782
2584
  fiber: Fiber,
1783
2585
  parent: ParentNode,
@@ -1798,6 +2600,7 @@ function commitHostDirtyFiber(
1798
2600
  return;
1799
2601
  }
1800
2602
 
2603
+ const isDomElement = isDomHostElement(element);
1801
2604
  const props = fiber.pendingProps as Record<string, unknown>;
1802
2605
  const previousProps = fiber.memoizedProps as Record<string, unknown> | undefined;
1803
2606
  const directTextChild =
@@ -1823,7 +2626,7 @@ function commitHostDirtyFiber(
1823
2626
  fiber.hydrateExisting !== true &&
1824
2627
  isRowTextOnlyUpdate(fiber.memoizedProps, props);
1825
2628
 
1826
- if (isDomHostElement(element) && !propsAreUnchanged && !propsAreChildrenOnly && !textOnlyRowUpdate) {
2629
+ if (isDomElement && !propsAreUnchanged && !propsAreChildrenOnly && !textOnlyRowUpdate) {
1827
2630
  applyProps(element, props, path, {
1828
2631
  ...options,
1829
2632
  eventRoot,
@@ -1841,16 +2644,16 @@ function commitHostDirtyFiber(
1841
2644
  ) {
1842
2645
  const childNodes = commitHostChildren(fiber.child, element, eventRoot, `${path}.c`, options);
1843
2646
  if (
1844
- !(isDomHostElement(element) && childNodes.length === 0 && committedPortalContainers.has(element)) &&
1845
- !(isDomHostElement(element) && shouldPreserveContentEditableChildren(element, props, childNodes))
2647
+ !(isDomElement && childNodes.length === 0 && committedPortalContainers.has(element)) &&
2648
+ !(isDomElement && shouldPreserveContentEditableChildren(element, props, childNodes))
1846
2649
  ) {
1847
2650
  syncChildNodes(element as ParentNode, childNodes);
1848
2651
  }
1849
2652
  } else if (fiber.subtreeFlags !== NoFlags) {
1850
- commitHostDirtyChildren(fiber.child, element, eventRoot, `${path}.c`, options);
2653
+ commitHostDirtyChildrenOf(fiber, fiber.child, element, eventRoot, `${path}.c`, options);
1851
2654
  }
1852
2655
 
1853
- if (isDomHostElement(element)) {
2656
+ if (isDomElement && isFormHostType(fiber.type)) {
1854
2657
  applyPostChildFormProps(element, props, previousProps);
1855
2658
  }
1856
2659
  applyChangedRef(previousProps?.ref, props.ref, element);
@@ -1890,7 +2693,8 @@ function commitHostDirtyFiber(
1890
2693
  syncOwnedChildNodes(container as ParentNode, previousNodes, childNodes);
1891
2694
  fiber.memoizedState = childNodes;
1892
2695
  } else {
1893
- commitHostDirtyChildren(
2696
+ commitHostDirtyChildrenOf(
2697
+ fiber,
1894
2698
  fiber.child,
1895
2699
  container as ParentNode,
1896
2700
  portalEventRoot,
@@ -1904,8 +2708,14 @@ function commitHostDirtyFiber(
1904
2708
  return;
1905
2709
  }
1906
2710
 
2711
+ if (fiber.tag === "reactive-dom-block") {
2712
+ fiber.memoizedProps = fiber.pendingProps;
2713
+ finishCommittedFiber(fiber);
2714
+ return;
2715
+ }
2716
+
1907
2717
  if (fiber.subtreeFlags !== NoFlags) {
1908
- commitHostDirtyChildren(fiber.child, parent, eventRoot, path, options);
2718
+ commitHostDirtyChildrenOf(fiber, fiber.child, parent, eventRoot, path, options);
1909
2719
  }
1910
2720
  fiber.memoizedProps = fiber.pendingProps;
1911
2721
  finishCommittedFiber(fiber);
@@ -2072,7 +2882,10 @@ function readAppendSuffixCommitHint(value: unknown): AppendSuffixCommitHint | un
2072
2882
  }
2073
2883
 
2074
2884
  function commitHostSingleRemoval(fiber: Fiber, parent: ParentNode): boolean {
2075
- const removed = getSingleRemovedFiber(fiber.alternate?.child, fiber.child);
2885
+ const removed =
2886
+ fiber.deletions?.length === 1
2887
+ ? fiber.deletions[0]
2888
+ : getSingleRemovedFiber(fiber.alternate?.child, fiber.child);
2076
2889
 
2077
2890
  if (removed === undefined) {
2078
2891
  return false;
@@ -2089,6 +2902,10 @@ function commitHostSingleRemoval(fiber: Fiber, parent: ParentNode): boolean {
2089
2902
  removedAny = true;
2090
2903
  }
2091
2904
 
2905
+ if (removedAny) {
2906
+ fiber.deletions = undefined;
2907
+ }
2908
+
2092
2909
  return removedAny;
2093
2910
  }
2094
2911
 
@@ -2166,6 +2983,11 @@ function isSameFiberSlot(current: Fiber, next: Fiber): boolean {
2166
2983
  }
2167
2984
 
2168
2985
  function collectCommittedHostNodes(fiber: Fiber): Node[] {
2986
+ if (fiber.tag === "reactive-dom-block") {
2987
+ const node = getReactiveDomBlockNode(fiber.stateNode);
2988
+ return node === undefined ? [] : [node];
2989
+ }
2990
+
2169
2991
  if (
2170
2992
  (fiber.tag === "host-component" || fiber.tag === "host-text") &&
2171
2993
  fiber.stateNode instanceof Node
@@ -2185,6 +3007,10 @@ function collectCommittedHostNodes(fiber: Fiber): Node[] {
2185
3007
  }
2186
3008
 
2187
3009
  function hasCommittedHostNode(fiber: Fiber): boolean {
3010
+ if (fiber.tag === "reactive-dom-block") {
3011
+ return getReactiveDomBlockNode(fiber.stateNode) !== undefined;
3012
+ }
3013
+
2188
3014
  if (
2189
3015
  (fiber.tag === "host-component" || fiber.tag === "host-text") &&
2190
3016
  fiber.stateNode instanceof Node
@@ -2249,6 +3075,13 @@ function commitHostFiber(
2249
3075
  return [text];
2250
3076
  }
2251
3077
 
3078
+ if (fiber.tag === "reactive-dom-block") {
3079
+ const node = getReactiveDomBlockNode(fiber.stateNode);
3080
+ fiber.memoizedProps = fiber.pendingProps;
3081
+ finishCommittedFiber(fiber);
3082
+ return node === undefined ? [] : [node];
3083
+ }
3084
+
2252
3085
  if (fiber.tag === "host-component") {
2253
3086
  const element = fiber.stateNode;
2254
3087
 
@@ -2269,6 +3102,7 @@ function commitHostFiber(
2269
3102
  return [element];
2270
3103
  }
2271
3104
 
3105
+ const isDomElement = isDomHostElement(element);
2272
3106
  const props = fiber.pendingProps as Record<string, unknown>;
2273
3107
  const previousProps = fiber.memoizedProps as Record<string, unknown> | undefined;
2274
3108
  const directTextChild =
@@ -2294,7 +3128,7 @@ function commitHostFiber(
2294
3128
  fiber.hydrateExisting !== true &&
2295
3129
  isRowTextOnlyUpdate(fiber.memoizedProps, props);
2296
3130
 
2297
- if (isDomHostElement(element) && !propsAreUnchanged && !propsAreChildrenOnly && !textOnlyRowUpdate) {
3131
+ if (isDomElement && !propsAreUnchanged && !propsAreChildrenOnly && !textOnlyRowUpdate) {
2298
3132
  applyProps(element, props, path, {
2299
3133
  ...options,
2300
3134
  eventRoot,
@@ -2313,8 +3147,8 @@ function commitHostFiber(
2313
3147
  ) {
2314
3148
  const childNodes = commitHostChildren(fiber.child, element, eventRoot, `${path}.c`, options);
2315
3149
  if (
2316
- !(isDomHostElement(element) && childNodes.length === 0 && committedPortalContainers.has(element)) &&
2317
- !(isDomHostElement(element) && shouldPreserveContentEditableChildren(element, props, childNodes))
3150
+ !(isDomElement && childNodes.length === 0 && committedPortalContainers.has(element)) &&
3151
+ !(isDomElement && shouldPreserveContentEditableChildren(element, props, childNodes))
2318
3152
  ) {
2319
3153
  syncChildNodes(element as ParentNode, childNodes);
2320
3154
  }
@@ -2322,7 +3156,7 @@ function commitHostFiber(
2322
3156
  commitHostChildren(fiber.child, element, eventRoot, `${path}.c`, options);
2323
3157
  }
2324
3158
 
2325
- if (isDomHostElement(element)) {
3159
+ if (isDomElement && isFormHostType(fiber.type)) {
2326
3160
  applyPostChildFormProps(element, props, previousProps);
2327
3161
  }
2328
3162
  applyChangedRef(previousProps?.ref, props.ref, element);
@@ -2460,6 +3294,36 @@ function finishCommittedFiber(fiber: Fiber): void {
2460
3294
  fiber.hostChildListChanged = false;
2461
3295
  }
2462
3296
 
3297
+ function getReactiveDomBlockNode(state: unknown): ChildNode | undefined {
3298
+ if (
3299
+ typeof state === "object" &&
3300
+ state !== null &&
3301
+ "node" in state &&
3302
+ (state as { node?: unknown }).node instanceof Node
3303
+ ) {
3304
+ return (state as { node: ChildNode }).node;
3305
+ }
3306
+
3307
+ return undefined;
3308
+ }
3309
+
3310
+ function disposeReactiveDomBlockState(
3311
+ state: unknown,
3312
+ seen: Set<unknown>,
3313
+ ): void {
3314
+ if (typeof state !== "object" || state === null || seen.has(state)) {
3315
+ return;
3316
+ }
3317
+
3318
+ seen.add(state);
3319
+ const dispose = (state as { dispose?: unknown }).dispose;
3320
+
3321
+ if (typeof dispose === "function") {
3322
+ dispose();
3323
+ (state as { dispose?: unknown }).dispose = undefined;
3324
+ }
3325
+ }
3326
+
2463
3327
  function hasChildListMutation(fiber: Fiber): boolean {
2464
3328
  return fiber.childListChanged || fiber.subtreeChildListChanged;
2465
3329
  }
@@ -2659,6 +3523,13 @@ function getDirectHostTextChild(children: unknown): string | undefined {
2659
3523
  : undefined;
2660
3524
  }
2661
3525
 
3526
+ // Only these host tags carry post-child form value/checked semantics. Gating on
3527
+ // the (cheap, already-known) type string lets the commit skip applyPostChildFormProps
3528
+ // and its instanceof probes for every other element.
3529
+ function isFormHostType(type: unknown): boolean {
3530
+ return type === "input" || type === "textarea" || type === "select";
3531
+ }
3532
+
2662
3533
  // This package has no Node type dependency; declare the minimal process
2663
3534
  // shape needed for the literal process.env.NODE_ENV expression below.
2664
3535
  declare const process: { env: Record<string, string | undefined> };
@@ -3138,13 +4009,26 @@ function tryReuseMemoBailout(
3138
4009
  return undefined;
3139
4010
  }
3140
4011
 
3141
- const fiber = getMemoBailoutFiber(
3142
- runtime,
3143
- current,
3144
- node.props,
3145
- previousMemoState,
3146
- canReuseCurrentFiber,
3147
- );
4012
+ if (memoStateNeedsActiveInstanceMark(previousMemoState)) {
4013
+ markActiveInstanceKeys(runtime, previousMemoState.instanceKeys);
4014
+ }
4015
+
4016
+ const fiber =
4017
+ canReuseCurrentFiber &&
4018
+ current.hasRefSubtree !== true &&
4019
+ current.hasDisposableResources !== true &&
4020
+ current.hydrateExisting !== true
4021
+ ? current
4022
+ : createWorkInProgress(current, node.props);
4023
+
4024
+ if (fiber === current) {
4025
+ current.pendingProps = node.props;
4026
+ current.flags = NoFlags;
4027
+ current.subtreeFlags = NoFlags;
4028
+ current.childListChanged = false;
4029
+ current.subtreeChildListChanged = false;
4030
+ current.hostChildListChanged = false;
4031
+ }
3148
4032
  fiber.type = node.type;
3149
4033
  fiber.child = getSkippedChild(current);
3150
4034
  fiber.memoizedState = previousMemoState;
@@ -3511,6 +4395,35 @@ function collectInstanceKeys(runtime: RootRuntime, prefix: string): string[] {
3511
4395
  return collectRuntimeInstanceKeys(runtime, prefix);
3512
4396
  }
3513
4397
 
4398
+ function collectMemoInstanceKeys(runtime: RootRuntime, prefix: string): string[] {
4399
+ return readDependencyFreeMemoInstanceKey(runtime, prefix) === undefined
4400
+ ? collectInstanceKeys(runtime, prefix)
4401
+ : emptyInstanceKeys;
4402
+ }
4403
+
4404
+ function readDependencyFreeMemoInstanceKey(
4405
+ runtime: RootRuntime,
4406
+ prefix: string,
4407
+ ): string | undefined {
4408
+ const keys = runtime.instanceKeysByPrefix.get(prefix);
4409
+
4410
+ if (keys === undefined || keys.size !== 1 || !keys.has(prefix)) {
4411
+ return undefined;
4412
+ }
4413
+
4414
+ const instance = runtime.instances.get(prefix) as RuntimeInstanceLike | undefined;
4415
+
4416
+ if (
4417
+ instance === undefined ||
4418
+ instance.contextDependencies !== undefined ||
4419
+ (instance.hooks !== undefined && instance.hooks.length > 0)
4420
+ ) {
4421
+ return undefined;
4422
+ }
4423
+
4424
+ return prefix;
4425
+ }
4426
+
3514
4427
  function markActiveInstanceKeys(runtime: RootRuntime, keys: readonly string[]): void {
3515
4428
  for (const key of keys) {
3516
4429
  runtime.activeInstanceKeys?.add(key);
@@ -3550,6 +4463,10 @@ function getMemoBailoutFiber(
3550
4463
  state: MemoFiberState,
3551
4464
  canReuseCurrentFiber: boolean,
3552
4465
  ): Fiber {
4466
+ if (memoStateNeedsActiveInstanceMark(state)) {
4467
+ markActiveInstanceKeys(runtime, state.instanceKeys);
4468
+ }
4469
+
3553
4470
  if (canReuseCurrentFiber && canReuseMemoBailoutFiber(current, state)) {
3554
4471
  current.pendingProps = pendingProps;
3555
4472
  current.flags = NoFlags;
@@ -3561,9 +4478,6 @@ function getMemoBailoutFiber(
3561
4478
  }
3562
4479
 
3563
4480
  const fiber = createWorkInProgress(current, pendingProps);
3564
- if (memoStateNeedsActiveInstanceMark(state)) {
3565
- markActiveInstanceKeys(runtime, state.instanceKeys);
3566
- }
3567
4481
  return fiber;
3568
4482
  }
3569
4483
 
@@ -3571,6 +4485,7 @@ function canReuseMemoBailoutFiber(current: Fiber, state: MemoFiberState): boolea
3571
4485
  return (
3572
4486
  state.hasRetainedInstanceDependencies === false &&
3573
4487
  current.hasRefSubtree !== true &&
4488
+ current.hasDisposableResources !== true &&
3574
4489
  current.hydrateExisting !== true
3575
4490
  );
3576
4491
  }