@tanstack/redact 0.0.20 → 0.1.0

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 (253) hide show
  1. package/dist/core/attributes.js +2 -1
  2. package/dist/core/attributes.js.map +2 -2
  3. package/dist/core/browser.d.ts +9 -0
  4. package/dist/core/browser.js +35 -0
  5. package/dist/core/browser.js.map +7 -0
  6. package/dist/core/index.d.ts +2 -2
  7. package/dist/core/index.js +3 -3
  8. package/dist/core/internal.d.ts +31 -8
  9. package/dist/core/internal.js +3 -1
  10. package/dist/core/internal.js.map +2 -2
  11. package/dist/core/resource-hints.d.ts +7 -0
  12. package/dist/core/resource-hints.js +19 -0
  13. package/dist/core/resource-hints.js.map +7 -0
  14. package/dist/core/types.js +1 -1
  15. package/dist/dom/_all.d.ts +11 -16
  16. package/dist/dom/_all.js +9 -20
  17. package/dist/dom/_all.js.map +2 -2
  18. package/dist/dom/client.d.ts +4 -4
  19. package/dist/dom/client.js +4 -4
  20. package/dist/dom/commit.d.ts +23 -0
  21. package/dist/dom/commit.js +191 -0
  22. package/dist/dom/commit.js.map +7 -0
  23. package/dist/dom/dispatcher.d.ts +4 -22
  24. package/dist/dom/dispatcher.js +184 -126
  25. package/dist/dom/dispatcher.js.map +3 -3
  26. package/dist/dom/dom.d.ts +2 -0
  27. package/dist/dom/dom.js +72 -52
  28. package/dist/dom/dom.js.map +2 -2
  29. package/dist/dom/error-info.d.ts +2 -0
  30. package/dist/dom/error-info.js +16 -0
  31. package/dist/dom/error-info.js.map +7 -0
  32. package/dist/dom/event-replay.js +1 -1
  33. package/dist/dom/features/activity/full.d.ts +1 -0
  34. package/dist/dom/features/activity/full.js +162 -0
  35. package/dist/dom/features/activity/full.js.map +7 -0
  36. package/dist/dom/features/activity/index.d.ts +1 -0
  37. package/dist/dom/features/activity/index.js +3 -0
  38. package/dist/dom/features/activity/index.js.map +7 -0
  39. package/dist/dom/features/activity/stub.d.ts +1 -0
  40. package/dist/dom/features/activity/stub.js +12 -0
  41. package/dist/dom/features/activity/stub.js.map +7 -0
  42. package/dist/dom/features/class/full.js +87 -21
  43. package/dist/dom/features/class/full.js.map +2 -2
  44. package/dist/dom/features/class/index.d.ts +1 -1
  45. package/dist/dom/features/class/index.js +2 -2
  46. package/dist/dom/features/class/stub.js +3 -3
  47. package/dist/dom/features/context/full.d.ts +4 -1
  48. package/dist/dom/features/context/full.js +69 -9
  49. package/dist/dom/features/context/full.js.map +2 -2
  50. package/dist/dom/features/context/index.d.ts +1 -1
  51. package/dist/dom/features/context/index.js +2 -2
  52. package/dist/dom/features/context/stub.d.ts +4 -1
  53. package/dist/dom/features/context/stub.js +15 -4
  54. package/dist/dom/features/context/stub.js.map +2 -2
  55. package/dist/dom/features/forward-ref/full.js +12 -21
  56. package/dist/dom/features/forward-ref/full.js.map +2 -2
  57. package/dist/dom/features/forward-ref/index.d.ts +1 -1
  58. package/dist/dom/features/forward-ref/index.js +2 -2
  59. package/dist/dom/features/forward-ref/stub.js +4 -4
  60. package/dist/dom/features/fragment-refs/index.d.ts +1 -0
  61. package/dist/dom/features/fragment-refs/index.js +8 -0
  62. package/dist/dom/features/fragment-refs/index.js.map +7 -0
  63. package/dist/dom/features/fragment-refs/stub.d.ts +3 -0
  64. package/dist/dom/features/fragment-refs/stub.js +14 -0
  65. package/dist/dom/features/fragment-refs/stub.js.map +7 -0
  66. package/dist/dom/features/hydration/full.d.ts +11 -6
  67. package/dist/dom/features/hydration/full.js +76 -76
  68. package/dist/dom/features/hydration/full.js.map +2 -2
  69. package/dist/dom/features/hydration/index.d.ts +1 -1
  70. package/dist/dom/features/hydration/index.js +2 -2
  71. package/dist/dom/features/hydration/stub.d.ts +7 -2
  72. package/dist/dom/features/hydration/stub.js +1 -1
  73. package/dist/dom/features/hydration/stub.js.map +1 -1
  74. package/dist/dom/features/index.d.ts +8 -7
  75. package/dist/dom/features/index.js +9 -8
  76. package/dist/dom/features/index.js.map +2 -2
  77. package/dist/dom/features/lazy/full.js +4 -4
  78. package/dist/dom/features/lazy/index.d.ts +1 -1
  79. package/dist/dom/features/lazy/index.js +2 -2
  80. package/dist/dom/features/lazy/stub.js +4 -4
  81. package/dist/dom/features/memo/full.js +8 -6
  82. package/dist/dom/features/memo/full.js.map +2 -2
  83. package/dist/dom/features/memo/index.d.ts +1 -1
  84. package/dist/dom/features/memo/index.js +2 -2
  85. package/dist/dom/features/memo/stub.js +4 -4
  86. package/dist/dom/features/portal/full.js +4 -4
  87. package/dist/dom/features/portal/index.d.ts +1 -1
  88. package/dist/dom/features/portal/index.js +2 -2
  89. package/dist/dom/features/portal/stub.js +4 -4
  90. package/dist/dom/features/suspense/full.js +192 -47
  91. package/dist/dom/features/suspense/full.js.map +2 -2
  92. package/dist/dom/features/suspense/index.d.ts +1 -1
  93. package/dist/dom/features/suspense/index.js +2 -2
  94. package/dist/dom/features/suspense/stub.js +4 -4
  95. package/dist/dom/features/view-transition/full.d.ts +6 -0
  96. package/dist/dom/features/view-transition/full.js +280 -0
  97. package/dist/dom/features/view-transition/full.js.map +7 -0
  98. package/dist/dom/features/view-transition/index.d.ts +1 -0
  99. package/dist/dom/features/view-transition/index.js +3 -0
  100. package/dist/dom/features/view-transition/index.js.map +7 -0
  101. package/dist/dom/features/view-transition/resources.d.ts +11 -0
  102. package/dist/dom/features/view-transition/resources.js +108 -0
  103. package/dist/dom/features/view-transition/resources.js.map +7 -0
  104. package/dist/dom/features/view-transition/stub.d.ts +6 -0
  105. package/dist/dom/features/view-transition/stub.js +22 -0
  106. package/dist/dom/features/view-transition/stub.js.map +7 -0
  107. package/dist/dom/features/view-transition/visual.d.ts +14 -0
  108. package/dist/dom/features/view-transition/visual.js +356 -0
  109. package/dist/dom/features/view-transition/visual.js.map +7 -0
  110. package/dist/dom/fragment-instance.d.ts +40 -0
  111. package/dist/dom/fragment-instance.js +324 -0
  112. package/dist/dom/fragment-instance.js.map +7 -0
  113. package/dist/dom/index.d.ts +11 -11
  114. package/dist/dom/index.js +11 -18
  115. package/dist/dom/index.js.map +3 -3
  116. package/dist/dom/portal.d.ts +2 -2
  117. package/dist/dom/portal.js +2 -2
  118. package/dist/dom/portal.js.map +1 -1
  119. package/dist/dom/reconcile.d.ts +32 -3
  120. package/dist/dom/reconcile.js +686 -222
  121. package/dist/dom/reconcile.js.map +2 -2
  122. package/dist/dom/resource-hints.d.ts +6 -0
  123. package/dist/dom/resource-hints.js +115 -0
  124. package/dist/dom/resource-hints.js.map +7 -0
  125. package/dist/dom/resource-store.d.ts +13 -0
  126. package/dist/dom/resource-store.js +43 -0
  127. package/dist/dom/resource-store.js.map +7 -0
  128. package/dist/dom/resources.d.ts +1 -0
  129. package/dist/dom/resources.js +44 -0
  130. package/dist/dom/resources.js.map +7 -0
  131. package/dist/dom/retained-effects.d.ts +6 -0
  132. package/dist/dom/retained-effects.js +49 -0
  133. package/dist/dom/retained-effects.js.map +7 -0
  134. package/dist/dom/root-internal.d.ts +4 -4
  135. package/dist/dom/root-internal.js +2 -2
  136. package/dist/dom/root-internal.js.map +2 -2
  137. package/dist/dom/root.d.ts +5 -5
  138. package/dist/dom/root.js +11 -11
  139. package/dist/dom/root.js.map +2 -2
  140. package/dist/dom/test-utils.js +1 -1
  141. package/dist/react/activity.d.ts +7 -0
  142. package/dist/react/activity.js +8 -0
  143. package/dist/react/activity.js.map +7 -0
  144. package/dist/react/children.d.ts +1 -1
  145. package/dist/react/children.js +3 -3
  146. package/dist/react/children.js.map +1 -1
  147. package/dist/react/class.d.ts +1 -1
  148. package/dist/react/class.js +3 -3
  149. package/dist/react/class.js.map +1 -1
  150. package/dist/react/compiler-runtime.js +2 -2
  151. package/dist/react/context.d.ts +1 -1
  152. package/dist/react/context.js +3 -3
  153. package/dist/react/context.js.map +1 -1
  154. package/dist/react/element.d.ts +6 -3
  155. package/dist/react/element.js +2 -2
  156. package/dist/react/element.js.map +2 -2
  157. package/dist/react/hooks.d.ts +26 -7
  158. package/dist/react/hooks.js +13 -3
  159. package/dist/react/hooks.js.map +2 -2
  160. package/dist/react/index.d.ts +39 -24
  161. package/dist/react/index.js +39 -19
  162. package/dist/react/index.js.map +3 -3
  163. package/dist/react/jsx-runtime.d.ts +2 -2
  164. package/dist/react/jsx-runtime.js +3 -3
  165. package/dist/react/memo.d.ts +1 -1
  166. package/dist/react/memo.js +1 -1
  167. package/dist/react/portal.js +1 -1
  168. package/dist/react/shared-internals.d.ts +15 -5
  169. package/dist/react/shared-internals.js +2 -3
  170. package/dist/react/shared-internals.js.map +2 -2
  171. package/dist/react/suspense.js +1 -1
  172. package/dist/react/view-transition.d.ts +32 -0
  173. package/dist/react/view-transition.js +13 -0
  174. package/dist/react/view-transition.js.map +7 -0
  175. package/dist/scheduler/index.js +1 -1
  176. package/dist/server/bootstrap-script.d.ts +1 -23
  177. package/dist/server/bootstrap-script.js +7 -6
  178. package/dist/server/bootstrap-script.js.map +2 -2
  179. package/dist/server/dispatcher.d.ts +28 -6
  180. package/dist/server/dispatcher.js +33 -15
  181. package/dist/server/dispatcher.js.map +2 -2
  182. package/dist/server/escape.js +16 -15
  183. package/dist/server/escape.js.map +2 -2
  184. package/dist/server/index.d.ts +6 -6
  185. package/dist/server/index.js +7 -7
  186. package/dist/server/renderToString.d.ts +1 -1
  187. package/dist/server/renderToString.js +12 -8
  188. package/dist/server/renderToString.js.map +2 -2
  189. package/dist/server/resource-hints.d.ts +22 -0
  190. package/dist/server/resource-hints.js +148 -0
  191. package/dist/server/resource-hints.js.map +7 -0
  192. package/dist/server/stream.d.ts +15 -2
  193. package/dist/server/stream.js +155 -63
  194. package/dist/server/stream.js.map +2 -2
  195. package/dist/server/walk.d.ts +15 -2
  196. package/dist/server/walk.js +143 -96
  197. package/dist/server/walk.js.map +2 -2
  198. package/dist/vite/index.d.ts +6 -0
  199. package/dist/vite/index.js +22 -6
  200. package/dist/vite/index.js.map +2 -2
  201. package/package.json +1 -1
  202. package/src/core/attributes.ts +1 -0
  203. package/src/core/browser.ts +33 -0
  204. package/src/core/internal.ts +24 -6
  205. package/src/core/resource-hints.ts +36 -0
  206. package/src/dom/_all.ts +1 -7
  207. package/src/dom/commit.ts +186 -0
  208. package/src/dom/dispatcher.ts +190 -134
  209. package/src/dom/dom.ts +70 -54
  210. package/src/dom/error-info.ts +15 -0
  211. package/src/dom/features/activity/full.ts +166 -0
  212. package/src/dom/features/activity/index.ts +1 -0
  213. package/src/dom/features/activity/stub.ts +14 -0
  214. package/src/dom/features/class/full.ts +72 -21
  215. package/src/dom/features/context/full.ts +66 -4
  216. package/src/dom/features/context/stub.ts +6 -0
  217. package/src/dom/features/forward-ref/full.ts +9 -19
  218. package/src/dom/features/fragment-refs/index.ts +1 -0
  219. package/src/dom/features/fragment-refs/stub.ts +3 -0
  220. package/src/dom/features/hydration/full.ts +84 -82
  221. package/src/dom/features/hydration/stub.ts +1 -1
  222. package/src/dom/features/index.ts +1 -0
  223. package/src/dom/features/memo/full.ts +4 -3
  224. package/src/dom/features/suspense/full.ts +187 -50
  225. package/src/dom/features/view-transition/full.ts +288 -0
  226. package/src/dom/features/view-transition/index.ts +1 -0
  227. package/src/dom/features/view-transition/resources.ts +107 -0
  228. package/src/dom/features/view-transition/stub.ts +7 -0
  229. package/src/dom/features/view-transition/visual.ts +406 -0
  230. package/src/dom/fragment-instance.ts +337 -0
  231. package/src/dom/index.ts +6 -7
  232. package/src/dom/portal.ts +1 -1
  233. package/src/dom/reconcile.ts +691 -244
  234. package/src/dom/resource-hints.ts +115 -0
  235. package/src/dom/resource-store.ts +52 -0
  236. package/src/dom/resources.ts +39 -0
  237. package/src/dom/retained-effects.ts +38 -0
  238. package/src/dom/root-internal.ts +4 -4
  239. package/src/dom/root.ts +12 -14
  240. package/src/react/activity.ts +10 -0
  241. package/src/react/element.ts +6 -2
  242. package/src/react/hooks.ts +12 -3
  243. package/src/react/index.ts +18 -0
  244. package/src/react/shared-internals.ts +3 -3
  245. package/src/react/view-transition.ts +41 -0
  246. package/src/server/bootstrap-script.ts +36 -5
  247. package/src/server/dispatcher.ts +35 -13
  248. package/src/server/escape.ts +15 -12
  249. package/src/server/renderToString.ts +7 -5
  250. package/src/server/resource-hints.ts +165 -0
  251. package/src/server/stream.ts +154 -62
  252. package/src/server/walk.ts +72 -11
  253. package/src/vite/index.ts +30 -13
@@ -8,17 +8,25 @@ import {
8
8
  type FiberRoot,
9
9
  type ReactElement,
10
10
  type ReactNode,
11
- type Hook,
12
11
  type Effect,
12
+ type Hook,
13
13
  } from '../core'
14
14
  import {
15
15
  ReactSharedInternals,
16
16
  REACT_LAZY_TYPE,
17
17
  REACT_STRICT_MODE_TYPE,
18
18
  REACT_PROFILER_TYPE,
19
+ REACT_VIEW_TRANSITION_TYPE,
19
20
  } from '../react'
20
- import { createHostNode, setProp } from './dom'
21
- import { makeDispatcher } from './dispatcher'
21
+ import { createHostNode, setProp, syncInputValue, syncTextareaValue } from './dom'
22
+ import { HOOK_BAILOUT, renderWithHooks } from './dispatcher'
23
+ import { rememberRetainedEffect, setLayoutDisconnected } from './retained-effects'
24
+ import { componentStack } from './error-info'
25
+ import { resourceKind } from '../core/resource-hints'
26
+ import { acquireResource } from './resources'
27
+ import { currentCommit, commitSynchronously, queueMutation, queueText, queueCommitEffects, setCommitCheckpointHook, onCommitRollback, onCommitFailure, checkpointCommit, rewindCommit, rememberChildList } from './commit'
28
+ import { markTransitionUpdate, deferTransition, cancelTransitions, deferTransitionLayout, deferTransitionPassive } from './features/view-transition'
29
+ import { createFragmentInstance, updateFragmentHost, refreshFragmentInstances } from './features/fragment-refs'
22
30
  import {
23
31
  adoptHostDom,
24
32
  adoptTextDom,
@@ -28,6 +36,7 @@ import {
28
36
  findHostParent as findHydrationHost,
29
37
  abortHydration,
30
38
  recoverHydration,
39
+ isHydrationBailout,
31
40
  } from './features/hydration'
32
41
 
33
42
  // ---------------------------------------------------------------------------
@@ -35,6 +44,7 @@ import {
35
44
  // ---------------------------------------------------------------------------
36
45
 
37
46
  let currentRoot: FiberRoot | null = null
47
+ let preparedParent: Node | null = null
38
48
  let flushing = false
39
49
  let isBatching = false
40
50
  const pendingRoots = new Set<FiberRoot>()
@@ -54,11 +64,21 @@ export function scheduleUpdate(fiber: Fiber): void {
54
64
  // query, any external store) can fire after unmount if their cleanup was
55
65
  // missed, and letting those reach rerenderFiber mounts zombie DOM into the
56
66
  // old .parent's DOM (which stays reachable via the stale pointer).
57
- if (fiber.um) return
67
+ if (fiber.um || fiber.pd) return
68
+ fiber.dy = true
69
+ // A component finishes its render-phase updates before reconciling children.
70
+ if (ReactSharedInternals.F === fiber) return
58
71
  const root = findRoot(fiber)
59
72
  if (!root) return
73
+ // Retained Suspense primaries must retry their boundary, not commit a child
74
+ // against the older props preserved by the failed attempt.
75
+ while (fiber.su) {
76
+ fiber = fiber.su
77
+ fiber.ms.u = true
78
+ fiber.dy = true
79
+ }
80
+ markTransitionUpdate(root)
60
81
  root.p.add(fiber)
61
- fiber.dy = true
62
82
  pendingRoots.add(root)
63
83
  if (isBatching) return
64
84
  if (!root.s) {
@@ -68,14 +88,19 @@ export function scheduleUpdate(fiber: Fiber): void {
68
88
  }
69
89
 
70
90
  export function flushSyncWork(fn: () => void): void {
91
+ cancelTransitions()
92
+ const transition = ReactSharedInternals.T
93
+ ReactSharedInternals.T = null
71
94
  const wasBatching = isBatching
72
95
  isBatching = true
73
96
  try {
74
97
  fn()
98
+ isBatching = wasBatching
99
+ flushPending()
75
100
  } finally {
76
101
  isBatching = wasBatching
102
+ ReactSharedInternals.T = transition
77
103
  }
78
- flushPending()
79
104
  }
80
105
 
81
106
  export function batchedUpdates<T>(fn: () => T): T {
@@ -105,27 +130,11 @@ function flushPending(): void {
105
130
  pendingRoots.clear()
106
131
  for (const root of roots) {
107
132
  root.s = false
108
- // Render each pending fiber from shallowest first so an ancestor's
109
- // cascade reaches descendants before we try to render them directly.
110
- // Descendants rendered via cascade still have `dy=true` (only
111
- // rerenderFiber clears it); when we later reach them in this loop,
112
- // rerenderFiber's own `if (!dy) return` is our short-circuit. We
113
- // previously filtered descendants of dy ancestors here, but that
114
- // loses updates whenever an ancestor's render doesn't actually reach
115
- // the descendant — e.g. React.memo bailing on equal props. Keep all
116
- // dy fibers and let rerenderFiber de-dupe via its dy check.
117
- const pending = [...root.p]
118
- root.p.clear()
119
- pending.sort((a, b) => fiberDepth(a) - fiberDepth(b))
120
- for (const fiber of pending) {
121
- try {
122
- rerenderFiber(fiber, root)
123
- } catch (error) {
124
- if (!recoverHydration(root, error)) throw error
125
- break
126
- }
133
+ const render = () => flushRoot(root)
134
+ if (!deferTransition(root, render, flushPending)) {
135
+ try { commitSynchronously(render) }
136
+ catch (error) { if (!recoverRootError(error)) throw error }
127
137
  }
128
- runEffects(root)
129
138
  }
130
139
  }
131
140
  } finally {
@@ -133,26 +142,80 @@ function flushPending(): void {
133
142
  }
134
143
  }
135
144
 
145
+ function flushRoot(root: FiberRoot): void {
146
+ if (root.er) {
147
+ flushRootErrors(root)
148
+ return
149
+ }
150
+ const render = root.u
151
+ root.u = undefined
152
+ if (render) render()
153
+ // Ancestors run first, but keep descendants in the queue: an ancestor can
154
+ // bail out without reaching a descendant's independently scheduled update.
155
+ const pending = [...root.p]
156
+ root.p.clear()
157
+ pending.sort((a, b) => fiberDepth(a) - fiberDepth(b))
158
+ for (const fiber of pending) {
159
+ if (!fiber.dy || fiber.um || fiber.pd) continue
160
+ const boundary = root.eb && findErrorBoundary(fiber)
161
+ const checkpoint = boundary || (root.sp && findSuspenseBoundary(fiber)) ? checkpointCommit(fiber) : -1
162
+ try {
163
+ rerenderFiber(fiber, root)
164
+ } catch (error) {
165
+ if (error instanceof RenderSuspenseCapture) {
166
+ rewindCommit(checkpoint)
167
+ error.boundary.ms.u = true
168
+ scheduleUpdate(error.boundary)
169
+ continue
170
+ }
171
+ if (error instanceof RenderErrorCapture) {
172
+ if (error.boundary.tag === FiberTag.Root) throw error
173
+ rewindCommit(checkpoint)
174
+ captureError(error)
175
+ continue
176
+ }
177
+ if (!recoverHydration(root, error)) throw error
178
+ break
179
+ }
180
+ }
181
+ runEffects(root)
182
+ }
183
+
184
+ export function scheduleRootRender(root: FiberRoot, render: () => void): void {
185
+ markTransitionUpdate(root)
186
+ root.u = render
187
+ pendingRoots.add(root)
188
+ if (isBatching) return
189
+ flushPending()
190
+ }
191
+
136
192
  export function discardPendingWork(root: FiberRoot): void {
193
+ root.u = undefined
137
194
  root.p.clear()
138
195
  root.s = false
139
196
  pendingRoots.delete(root)
140
197
  }
141
198
 
142
- function fiberDepth(fiber: Fiber): number {
143
- let d = 0
144
- let p: Fiber | null = fiber.parent
145
- while (p) {
146
- d++
147
- p = p.parent
199
+ export function discardPendingEffects(root: FiberRoot): void {
200
+ for (const queue of [pendingEffects, pendingInsertionEffects, pendingCommits]) {
201
+ for (let index = queue.length - 1; index >= 0; index--) {
202
+ if (findRoot(queue[index]![0]) === root) removePendingEffect(queue, index)
203
+ }
148
204
  }
149
- return d
205
+ for (const fiber of stagedCommits.keys()) {
206
+ if (findRoot(fiber) === root) removeStagedCommits(fiber)
207
+ }
208
+ }
209
+
210
+ function fiberDepth(fiber: Fiber): number {
211
+ // Reused fibers keep their parent and root, including keyed sibling moves.
212
+ return fiber.depth ??= fiber.parent ? fiberDepth(fiber.parent) + 1 : 0
150
213
  }
151
214
 
152
215
  export function findRoot(fiber: Fiber): FiberRoot | null {
153
216
  let f: Fiber | null = fiber
154
217
  while (f) {
155
- if (f.root) return f.root
218
+ if (f.root) return fiber.root = f.root
156
219
  f = f.parent
157
220
  }
158
221
  return null
@@ -163,6 +226,7 @@ export function findRoot(fiber: Fiber): FiberRoot | null {
163
226
  // ---------------------------------------------------------------------------
164
227
 
165
228
  export function renderRoot(root: FiberRoot, children: ReactNode): void {
229
+ if (!currentCommit) return commitSynchronously(() => renderRoot(root, children))
166
230
  const rootFiber = root.r
167
231
  rootFiber.pp = { children }
168
232
  currentRoot = root
@@ -185,7 +249,7 @@ function rerenderFiber(fiber: Fiber, root: FiberRoot): void {
185
249
  // content from a previous location staying on screen after nav, because a
186
250
  // pending rerender on the old route's LibraryLandingPage (um during
187
251
  // Outlet's shallow-first render) still fires from root.pending.
188
- if (fiber.um) return
252
+ if (fiber.um || fiber.pd) return
189
253
  // Clear BEFORE rendering so a scheduleUpdate() triggered mid-render (e.g.
190
254
  // error boundary catching a descendant throw) marks us dy for the next
191
255
  // flush iteration instead of being wiped out when render() completes.
@@ -204,7 +268,8 @@ function rerenderFiber(fiber: Fiber, root: FiberRoot): void {
204
268
  const prevForcing = forceRerenderingFiber
205
269
  forceRerenderingFiber = fiber
206
270
  try {
207
- renderFiber(fiber, getHostParent(fiber), getAnchor(fiber))
271
+ const domParent = getHostParent(fiber)
272
+ renderFiber(fiber, domParent, getAnchor(fiber, domParent))
208
273
  } finally {
209
274
  forceRerenderingFiber = prevForcing
210
275
  if (resumeHydration) {
@@ -255,10 +320,6 @@ function pushChildren(node: ReactNode, out: NormalizedChild[]): void {
255
320
  for (let i = 0; i < node.length; i++) pushChildren(node[i], out)
256
321
  return
257
322
  }
258
- if (isIterable(node)) {
259
- for (const item of node as Iterable<ReactNode>) pushChildren(item, out)
260
- return
261
- }
262
323
  if (typeof node === 'object') {
263
324
  const t = (node as any).$$typeof
264
325
  if (ACCEPTED_ELEMENT_MARKERS.has(t)) {
@@ -278,6 +339,9 @@ function pushChildren(node: ReactNode, out: NormalizedChild[]): void {
278
339
  return
279
340
  }
280
341
  }
342
+ if (isIterable(node)) {
343
+ for (const item of node as Iterable<ReactNode>) pushChildren(item, out)
344
+ }
281
345
  }
282
346
 
283
347
  function isIterable(obj: any): boolean {
@@ -318,7 +382,7 @@ function fiberFromChild(child: NormalizedChild, parent: Fiber): Fiber {
318
382
  const marker = type && (type as any).$$typeof
319
383
  if (typeof type === 'string') tag = FiberTag.Host
320
384
  else if (type === REACT_FRAGMENT_TYPE) tag = FiberTag.Fragment
321
- else if (type === REACT_STRICT_MODE_TYPE || type === REACT_PROFILER_TYPE) tag = FiberTag.Fragment
385
+ else if (type === REACT_STRICT_MODE_TYPE || type === REACT_PROFILER_TYPE || type === REACT_VIEW_TRANSITION_TYPE) tag = FiberTag.Fragment
322
386
  else {
323
387
  // Feature-registered type matchers (Portal, future extractions). Features
324
388
  // that carry the symbol as element.type directly (rather than wrapping in
@@ -355,8 +419,9 @@ export function reconcileChildren(
355
419
  domParent: Node,
356
420
  anchor: Node | null,
357
421
  ): void {
358
- // Fast path: unkeyed positional steady-state. Walk the existing sibling
359
- // chain and newChildren in lockstep, validating AND committing in one pass.
422
+ // Fast path: positional steady-state, including unchanged keyed lists.
423
+ // Walk the sibling chain and newChildren in lockstep, validating and
424
+ // committing in one pass.
360
425
  // On any divergence we fall back to the slow path, which rebuilds the
361
426
  // sibling chain anyway — partial pp writes are idempotent.
362
427
  // Skips the Map / Set / existing-array allocation entirely.
@@ -365,12 +430,12 @@ export function reconcileChildren(
365
430
  let ok = true
366
431
  for (let i = 0; i < newChildren.length; i++) {
367
432
  const child = newChildren[i]
368
- if (child == null || !f || f.key != null) { ok = false; break }
433
+ if (child == null || !f) { ok = false; break }
369
434
  if (typeof child === 'string') {
370
- if (f.tag !== FiberTag.Text) { ok = false; break }
435
+ if (f.tag !== FiberTag.Text || f.key != null) { ok = false; break }
371
436
  f.pp = child
372
437
  } else {
373
- if ((child as ReactElement).key != null) { ok = false; break }
438
+ if (!sameKey(f.key, (child as ReactElement).key)) { ok = false; break }
374
439
  if (f.type !== (child as ReactElement).type) { ok = false; break }
375
440
  f.pp = (child as ReactElement).props
376
441
  f.ref = (child as any).ref ?? null
@@ -380,19 +445,32 @@ export function reconcileChildren(
380
445
  if (ok && f === null) {
381
446
  // Pass 2: render forward with per-child anchors. Identical to the slow
382
447
  // path's pass 2.
383
- for (let r: Fiber | null = parent.child; r; r = r.sibling) {
384
- let a = anchor
385
- for (let s: Fiber | null = r.sibling; s; s = s.sibling) {
386
- const d = firstDomNode(s)
387
- if (d && d.parentNode === domParent) { a = d; break }
388
- }
389
- renderFiber(r, domParent, a)
390
- }
448
+ renderChildChain(parent, domParent, anchor, false)
391
449
  return
392
450
  }
393
451
  }
394
452
 
453
+ if (!parent.child) {
454
+ // Build every sibling before rendering, just like the matching path.
455
+ // A first mount has no old fibers to index, claim or remove.
456
+ rememberChildList(parent)
457
+ let previous: Fiber | null = null
458
+ for (const child of newChildren) {
459
+ if (child == null) continue
460
+ const fiber = fiberFromChild(child, parent)
461
+ if (previous) previous.sibling = fiber
462
+ else parent.child = fiber
463
+ previous = fiber
464
+ }
465
+ renderChildChain(parent, domParent, anchor, true)
466
+ if (previous && !currentRoot?.h && domParent.nodeName !== 'HEAD') {
467
+ placeChildrenInOrder(parent, domParent, anchor)
468
+ }
469
+ return
470
+ }
471
+
395
472
  const existing = collectChildren(parent)
473
+ rememberChildList(parent, existing)
396
474
  const keyed = new Map<string, Fiber>()
397
475
  for (const f of existing) {
398
476
  if (f.key != null) keyed.set('k' + f.key, f)
@@ -450,7 +528,7 @@ export function reconcileChildren(
450
528
  if (!match) {
451
529
  while (existingIdx < existing.length) {
452
530
  const cand = existing[existingIdx]!
453
- if (claimed.has(cand) || cand.key != null) {
531
+ if (cand.key != null) {
454
532
  existingIdx++
455
533
  continue
456
534
  }
@@ -503,17 +581,10 @@ export function reconcileChildren(
503
581
  // walks DOM forward and each renderFiber adopts the next existing node,
504
582
  // so per-child anchors are moot — fall back to the parent's anchor.
505
583
  const hydrating = !!currentRoot?.h
506
- for (let f: Fiber | null = parent.child; f; f = f.sibling) {
507
- let a = anchor
508
- if (!hydrating) {
509
- // Find the firstDomNode of the next still-mounted sibling, if any.
510
- for (let s: Fiber | null = f.sibling; s; s = s.sibling) {
511
- const d = firstDomNode(s)
512
- if (d && d.parentNode === domParent) { a = d; break }
513
- }
514
- }
515
- renderFiber(f, domParent, a)
516
- }
584
+ // New siblings have no DOM yet, so searching them for an anchor cannot help.
585
+ // An empty replacement has no new chain. The old children remain linked
586
+ // until cleanup below, and must not render again on their way out.
587
+ if (prevNewFiber) renderChildChain(parent, domParent, anchor, hydrating || existing.length === 0)
517
588
 
518
589
  if (!prevNewFiber) parent.child = null
519
590
  else prevNewFiber.sibling = null
@@ -535,13 +606,6 @@ export function reconcileChildren(
535
606
  structurallyChanged = true
536
607
  }
537
608
  }
538
- // Leftover keyed
539
- for (const f of keyed.values()) {
540
- if (!claimed.has(f)) {
541
- unmountFiber(f, domParent)
542
- structurallyChanged = true
543
- }
544
- }
545
609
  }
546
610
 
547
611
  // During hydration, DOM is already in document order from the cursor-driven
@@ -560,14 +624,49 @@ export function reconcileChildren(
560
624
  }
561
625
  }
562
626
 
627
+ function renderChildChain(
628
+ parent: Fiber,
629
+ domParent: Node,
630
+ anchor: Node | null,
631
+ skipAnchors: boolean,
632
+ ): void {
633
+ for (let f = parent.child; f; f = f.sibling) {
634
+ let nextAnchor = anchor
635
+ if (!skipAnchors) {
636
+ for (let sibling = f.sibling; sibling; sibling = sibling.sibling) {
637
+ const dom = firstDomNode(sibling, domParent)
638
+ if (dom) {
639
+ nextAnchor = dom
640
+ break
641
+ }
642
+ }
643
+ }
644
+ renderFiber(f, domParent, nextAnchor)
645
+ }
646
+ }
647
+
563
648
  function placeChildrenInOrder(parent: Fiber, domParent: Node, anchor: Node | null): void {
564
- const doms: Node[] = []
565
649
  let c = parent.child
650
+ // One direct DOM child already at its exact anchor needs no collection.
651
+ if ((!currentCommit || domParent === preparedParent) && c && !c.sibling && (c.tag === FiberTag.Host || c.tag === FiberTag.Text) &&
652
+ c.dom?.parentNode === domParent && c.dom.nextSibling === anchor) return
653
+ const doms: Node[] = []
566
654
  while (c) {
567
655
  collectHostDoms(c, doms)
568
656
  c = c.sibling
569
657
  }
570
658
 
659
+ if (currentCommit && domParent !== preparedParent) {
660
+ if (doms.length) queuePlacement(doms, domParent, anchor)
661
+ } else placeDomsInOrder(doms, domParent, anchor)
662
+ }
663
+
664
+ function queuePlacement(doms: Node[], parent: Node, anchor: Node | null): void {
665
+ queueMutation(() => placeDomsInOrder(doms, parent, anchor))
666
+ }
667
+
668
+ function placeDomsInOrder(doms: Node[], domParent: Node, anchor: Node | null): void {
669
+
571
670
  // Pre-check: if our fiber-owned DOM is already in document order within
572
671
  // domParent AND the trailing anchor matches, no reorder is needed. This is
573
672
  // the common case on stable re-renders, and avoids detaching/re-attaching
@@ -579,7 +678,7 @@ function placeChildrenInOrder(parent: Fiber, domParent: Node, anchor: Node | nul
579
678
  current = current!.nextSibling
580
679
  // Skip foreign nodes (SSR-injected scripts, dev-styles) between owned
581
680
  // fiber DOMs — they should stay where they are.
582
- while (current && !doms.includes(current as Node)) {
681
+ while (current && current !== doms[i] && !doms.includes(current as Node)) {
583
682
  current = current.nextSibling
584
683
  }
585
684
  if (current !== doms[i]) inOrder = false
@@ -636,6 +735,7 @@ function collectHostDoms(fiber: Fiber, out: Node[]): void {
636
735
  collectHostDoms(c, out)
637
736
  c = c.sibling
638
737
  }
738
+ if (fiber.tag === FiberTag.Suspense && fiber.ms?.f) collectHostDoms(fiber.ms.f, out)
639
739
  }
640
740
 
641
741
  function collectChildren(parent: Fiber): Fiber[] {
@@ -711,12 +811,27 @@ export function getForceRerenderingFiber(): Fiber | null {
711
811
 
712
812
  registerRenderer(FiberTag.Text, renderText)
713
813
  registerRenderer(FiberTag.Host, renderHost)
814
+ registerRenderer(FiberTag.Resource, renderHost)
714
815
  registerRenderer(FiberTag.Function, renderFunction)
715
816
  registerRenderer(FiberTag.Fragment, renderFragment)
716
817
 
717
818
  export function renderFiber(fiber: Fiber, domParent: Node, anchor: Node | null): void {
819
+ // Memo consumes pending state when it delegates to its inner renderer.
820
+ if (fiber.tag !== FiberTag.Memo) fiber.dy = false
821
+ fiber.root ||= currentRoot || findRoot(fiber)
822
+ if (fiber.root?.sp && fiber.parent?.ld && fiber.mp === null) setLayoutDisconnected(fiber, true)
823
+ if (fiber.root?.a) CAPABILITIES.prepareActivityFiber(fiber)
718
824
  const fn = RENDERERS[fiber.tag]
719
- if (fn) fn(fiber, domParent, anchor)
825
+ try {
826
+ if (fn) fn(fiber, domParent, anchor)
827
+ } catch (error) {
828
+ removeStagedCommits(fiber)
829
+ if (currentRoot?.h || error instanceof RenderErrorCapture || error instanceof RenderSuspenseCapture || isHydrationBailout(error)) throw error
830
+ if (isThenable(error)) handleSuspended(fiber, error)
831
+ else handleErrorInRender(fiber, error)
832
+ }
833
+ if (fiber.root?.a) CAPABILITIES.syncActivity(fiber)
834
+ if (lastStagedFiber === fiber) flushFiberCommits(fiber)
720
835
  }
721
836
 
722
837
  function renderText(fiber: Fiber, domParent: Node, anchor: Node | null): void {
@@ -729,32 +844,62 @@ function renderText(fiber: Fiber, domParent: Node, anchor: Node | null): void {
729
844
  fiber.dom = document.createTextNode(text)
730
845
  insertInto(domParent, fiber.dom, anchor)
731
846
  }
847
+ updateFragmentHost(fiber, true)
732
848
  } else {
733
849
  // Past the fast path, and adoptTextDom already realigned `.data` on
734
850
  // hydration — `.data !== text` here is guaranteed, so write directly.
735
- ;(fiber.dom as Text).data = text
851
+ queueText(fiber.dom as Text, text)
736
852
  }
737
853
  fiber.mp = text
738
854
  // dy cleared at rerender start; leaving true lets mid-render schedule persist
739
855
  }
740
856
 
741
857
  function renderHost(fiber: Fiber, domParent: Node, anchor: Node | null): void {
858
+ const hadDom = !!fiber.dom
742
859
  const props = fiber.pp ?? {}
743
860
  const prev = fiber.mp ?? {}
744
861
  const type = fiber.type as string
745
862
  const isSvg = type === 'svg' || (domParent as Element).namespaceURI === 'http://www.w3.org/2000/svg'
863
+ const isResource = fiber.tag === FiberTag.Resource
864
+ const canAcquire = !isSvg && (isResource || !fiber.mp) && resourceKind(type, props)
865
+ if (isResource || canAcquire) {
866
+ fiber.tag = FiberTag.Resource
867
+ if (canAcquire) {
868
+ let container: Node = domParent.nodeType === 9 ? domParent : domParent.ownerDocument!
869
+ if (type !== 'script') {
870
+ // Host assembly may still be detached. Style ownership follows the
871
+ // logical root or nearest portal, not that temporary DOM tree.
872
+ container = fiber.root?.c || domParent
873
+ for (let parent = fiber.parent; parent; parent = parent.parent) {
874
+ if (parent.tag === FiberTag.Portal) { container = parent.pp.container; break }
875
+ }
876
+ }
877
+ queueMutation(() => {
878
+ const resource = acquireResource(type, props, container)
879
+ fiber.sn ||= resource
880
+ })
881
+ }
882
+ if (fiber.mp) syncRefIfChanged(fiber, fiber.sn)
883
+ else attachRef(fiber, null)
884
+ fiber.mp = props
885
+ return
886
+ }
746
887
 
747
888
  // <select value> must be applied AFTER children mount — setting `.value`
748
889
  // on a `<select>` with no matching `<option>` yet resets it to empty. Same
749
890
  // for `defaultValue` on first mount. Stash and replay.
750
891
  const isSelect = type === 'select'
892
+ const isInput = !isSvg && type === 'input'
893
+ const isTextarea = !isSvg && type === 'textarea'
894
+ const deferValue = isSelect || isInput || isTextarea
895
+ let hydrated = false
751
896
  const deferredSelectValue =
752
897
  isSelect && (props.value !== undefined || props.defaultValue !== undefined)
753
898
  ? props.value !== undefined ? props.value : props.defaultValue
754
899
  : undefined
755
900
 
756
901
  if (!fiber.dom) {
757
- const hydrated = currentRoot?.h ? adoptHostDom(fiber, fiber.parent!) : false
902
+ hydrated = currentRoot?.h ? adoptHostDom(fiber, fiber.parent!) : false
758
903
  if (!hydrated) {
759
904
  fiber.dom = createHostNode(type, isSvg)
760
905
  // Two passes so form-control attributes (notably <input type>) are in
@@ -763,7 +908,7 @@ function renderHost(fiber: Fiber, domParent: Node, anchor: Node | null): void {
763
908
  // → `input` vs `change`); binding before `type` is applied would
764
909
  // attach to the wrong event for checkbox/radio/file inputs.
765
910
  for (const k in props) {
766
- if (isSelect && (k === 'value' || k === 'defaultValue')) continue
911
+ if ((deferValue && (k === 'value' || k === 'defaultValue')) || (isInput && (k === 'checked' || k === 'defaultChecked' || k === 'name'))) continue
767
912
  if (isEventProp(k)) continue
768
913
  setProp(fiber.dom as Element, k, props[k], undefined, isSvg)
769
914
  }
@@ -771,45 +916,30 @@ function renderHost(fiber: Fiber, domParent: Node, anchor: Node | null): void {
771
916
  if (!isEventProp(k)) continue
772
917
  setProp(fiber.dom as Element, k, props[k], undefined, isSvg)
773
918
  }
919
+ if (isInput) {
920
+ syncInputValue(fiber.dom as HTMLInputElement, props)
921
+ setProp(fiber.dom as Element, 'name', props.name, undefined, false)
922
+ }
923
+ if (isTextarea) syncTextareaValue(fiber.dom as HTMLTextAreaElement, props, true)
774
924
  insertInto(domParent, fiber.dom, anchor)
775
925
  }
926
+ updateFragmentHost(fiber, true)
776
927
  attachRef(fiber, fiber.dom)
777
- } else if (prev !== props) {
778
- const el = fiber.dom as Element
779
- // Single-pass diff. Defer changed event props into a small array so the
780
- // `type-before-events` invariant the mount path needs (setEventHandler
781
- // reads `el.type` to resolve onChange→input vs change) still holds when
782
- // a render flips both `type` and an event handler in the same pass.
783
- // The vast majority of host updates have no events at all (e.g. data-*
784
- // attributes flipping on a stable list), so the deferred array stays
785
- // null and we collapse to one for-in over `props`.
786
- let deferredEvents: string[] | null = null
787
- for (const k in props) {
788
- if (isSelect && (k === 'value' || k === 'defaultValue')) continue
789
- if (isEventProp(k)) {
790
- if (prev[k] !== props[k]) {
791
- deferredEvents ||= []
792
- deferredEvents.push(k)
793
- }
794
- continue
795
- }
796
- if (prev[k] !== props[k]) setProp(el, k, props[k], prev[k], isSvg)
797
- }
798
- // Removals — keys present in prev but not in props.
799
- for (const k in prev) {
800
- if (!(k in props)) setProp(el, k, undefined, prev[k], isSvg)
801
- }
802
- if (deferredEvents) {
803
- for (let i = 0; i < deferredEvents.length; i++) {
804
- const k = deferredEvents[i]!
805
- setProp(el, k, props[k], prev[k], isSvg)
806
- }
807
- }
808
- syncRefIfChanged(fiber, fiber.dom)
928
+ } else if (prev !== props && (isInput || isTextarea || hostNeedsUpdate(props, prev))) {
929
+ queueHostProps(fiber.dom as Element, props, prev, isSvg, isInput, isTextarea, deferValue)
809
930
  }
931
+ if (hadDom) syncRefIfChanged(fiber, fiber.dom)
810
932
 
811
933
  // Children go into this DOM node
812
- reconcileChildren(fiber, childrenToArray(props.children), fiber.dom!, null)
934
+ // Textarea's value/defaultValue owns its text node, not child fibers.
935
+ if (!hadDom && !hydrated) {
936
+ // Only a host allocated by this render is safe to assemble eagerly.
937
+ // User-owned detached roots and portal containers still commit normally.
938
+ const previous = preparedParent
939
+ preparedParent = fiber.dom
940
+ try { reconcileChildren(fiber, isTextarea ? [] : childrenToArray(props.children), fiber.dom!, null) }
941
+ finally { preparedParent = previous }
942
+ } else reconcileChildren(fiber, isTextarea ? [] : childrenToArray(props.children), fiber.dom!, null)
813
943
 
814
944
  // During hydration, if after reconciling all client-expected children we
815
945
  // still have server DOM left in the cursor for this host, that's a
@@ -820,7 +950,7 @@ function renderHost(fiber: Fiber, domParent: Node, anchor: Node | null): void {
820
950
  const parentTag = (fiber.type as string).toLowerCase()
821
951
  const hasOpaqueHydrationChildren =
822
952
  props.dangerouslySetInnerHTML != null ||
823
- (parentTag === 'textarea' && (props.value != null || props.defaultValue != null))
953
+ isTextarea
824
954
  if (
825
955
  parentTag !== 'head' &&
826
956
  parentTag !== 'html' &&
@@ -835,7 +965,7 @@ function renderHost(fiber: Fiber, domParent: Node, anchor: Node | null): void {
835
965
  ? `Hydration mismatch: server rendered extra nodes inside <${parentTag}>.`
836
966
  : 'Hydration mismatch.',
837
967
  )
838
- if (currentRoot.re) currentRoot.re(error)
968
+ if (currentRoot.re) currentRoot.re(error, { componentStack: null })
839
969
  abortHydration(error, fiber)
840
970
  }
841
971
  }
@@ -843,57 +973,86 @@ function renderHost(fiber: Fiber, domParent: Node, anchor: Node | null): void {
843
973
  }
844
974
 
845
975
  // Apply <select> value after options are mounted.
846
- if (isSelect && deferredSelectValue !== undefined) {
847
- const select = fiber.dom as HTMLSelectElement
848
- if (Array.isArray(deferredSelectValue)) {
849
- const asStrings = deferredSelectValue.map((v) => '' + v)
850
- for (const opt of Array.from(select.options)) {
851
- opt.selected = asStrings.includes(opt.value)
852
- }
853
- } else {
854
- select.value = '' + deferredSelectValue
855
- }
976
+ if (isSelect && !hydrated && deferredSelectValue !== undefined) {
977
+ queueSelectValue(fiber.dom as HTMLSelectElement, deferredSelectValue)
856
978
  }
857
979
 
858
980
  fiber.mp = props
859
981
  // dy cleared at rerender start; leaving true lets mid-render schedule persist
860
982
  }
861
983
 
862
- function renderFunction(fiber: Fiber, domParent: Node, anchor: Node | null): void {
863
- const prevDispatcher = ReactSharedInternals.H
864
- const prevFiber = ReactSharedInternals.F
865
- const prevHook = ReactSharedInternals.K
866
- const prevIndex = ReactSharedInternals.I
984
+ function queueSelectValue(select: HTMLSelectElement, value: any): void {
985
+ queueMutation(() => {
986
+ if (Array.isArray(value)) {
987
+ const selected = new Set(value.map((v) => '' + v))
988
+ const options = select.options
989
+ for (let i = 0; i < options.length; i++) {
990
+ const opt = options[i]!
991
+ opt.selected = selected.has(opt.value)
992
+ }
993
+ } else {
994
+ select.value = '' + value
995
+ }
996
+ })
997
+ }
867
998
 
868
- ReactSharedInternals.H = makeDispatcher()
869
- ReactSharedInternals.F = fiber
870
- ReactSharedInternals.K = null
871
- ReactSharedInternals.I = 0
999
+ function hostNeedsUpdate(props: any, previous: any): boolean {
1000
+ for (const key in props) {
1001
+ if (props[key] !== previous[key] && key !== 'children' && key !== 'ref' && key !== 'key') return true
1002
+ }
1003
+ for (const key in previous) {
1004
+ if (key !== 'children' && key !== 'ref' && key !== 'key' && !(key in props)) return true
1005
+ }
1006
+ return false
1007
+ }
872
1008
 
873
- let rendered: ReactNode
1009
+ function queueHostProps(el: Element, props: any, prev: any, isSvg: boolean, isInput: boolean, isTextarea: boolean, deferValue: boolean): void {
1010
+ queueMutation(() => {
1011
+ // Radio groups stay disconnected until type, value and checked agree.
1012
+ if (isInput) setProp(el, 'name', '', undefined, false)
1013
+ let events: string[] | undefined
1014
+ for (const key in props) {
1015
+ const next = props[key], previous = prev[key]
1016
+ if (next === previous || key === 'children' || key === 'ref' || key === 'key' || (deferValue && (key === 'value' || key === 'defaultValue')) || (isInput && (key === 'checked' || key === 'defaultChecked' || key === 'name'))) continue
1017
+ if (isEventProp(key)) {
1018
+ (events ||= []).push(key)
1019
+ } else setProp(el, key, next, previous, isSvg)
1020
+ }
1021
+ for (const key in prev) {
1022
+ if (key === 'children' || key === 'ref' || key === 'key' || ((isInput || isTextarea) && (key === 'value' || key === 'defaultValue')) || (isInput && (key === 'checked' || key === 'defaultChecked' || key === 'name'))) continue
1023
+ if (!(key in props)) setProp(el, key, undefined, prev[key], isSvg)
1024
+ }
1025
+ // Event resolution sees the new input type, as it does on mount.
1026
+ if (events) for (const key of events) setProp(el, key, props[key], prev[key], isSvg)
1027
+ if (isInput) {
1028
+ syncInputValue(el as HTMLInputElement, props, prev)
1029
+ setProp(el, 'name', props.name, prev.name, false)
1030
+ } else if (isTextarea) syncTextareaValue(el as HTMLTextAreaElement, props)
1031
+ })
1032
+ }
1033
+
1034
+ function renderFunction(fiber: Fiber, domParent: Node, anchor: Node | null): void {
1035
+ const canBail = fiber === forceRerenderingFiber && fiber.pp === fiber.mp
1036
+ fiber.cx?.clear()
1037
+ let rendered: ReactNode | typeof HOOK_BAILOUT
874
1038
  let deferredForHydration = false
875
1039
  try {
876
- rendered = (fiber.type as Function)(fiber.pp ?? {})
1040
+ rendered = renderWithHooks(fiber, fiber.type, fiber.pp ?? {}, undefined, canBail)
877
1041
  } catch (e: any) {
878
1042
  if (isThenable(e)) {
879
1043
  if (deferHydration(fiber, e)) {
880
1044
  deferredForHydration = true
881
1045
  } else {
882
- CAPABILITIES.handleSuspended(fiber, e)
883
- rendered = null
1046
+ handleSuspended(fiber, e)
1047
+ return
884
1048
  }
885
1049
  } else {
886
1050
  handleErrorInRender(fiber, e)
887
1051
  return
888
1052
  }
889
- } finally {
890
- ReactSharedInternals.H = prevDispatcher
891
- ReactSharedInternals.F = prevFiber
892
- ReactSharedInternals.K = prevHook
893
- ReactSharedInternals.I = prevIndex
894
1053
  }
895
1054
 
896
- if (deferredForHydration) return
1055
+ if (deferredForHydration || rendered === HOOK_BAILOUT) return
897
1056
 
898
1057
  reconcileChildren(fiber, childrenToArray(rendered), domParent, anchor)
899
1058
  fiber.mp = fiber.pp
@@ -930,6 +1089,24 @@ export function deferHydration(fiber: Fiber, thenable: Promise<any>): boolean {
930
1089
  function renderFragment(fiber: Fiber, domParent: Node, anchor: Node | null): void {
931
1090
  const props = fiber.pp ?? {}
932
1091
  reconcileChildren(fiber, childrenToArray(props.children), domParent, anchor)
1092
+ if (fiber.type === REACT_FRAGMENT_TYPE) {
1093
+ if (fiber.ref && !fiber.sn) fiber.sn = createFragmentInstance(fiber)
1094
+ const previous = fiber.ms?.ref
1095
+ if (previous !== fiber.ref) {
1096
+ if (fiber.cu) {
1097
+ const cleanups = fiber.cu
1098
+ queueMutation(() => {
1099
+ for (const cleanup of cleanups) {
1100
+ try { cleanup() } catch (error) { handleCommitError(fiber, error) }
1101
+ }
1102
+ })
1103
+ fiber.cu = null
1104
+ }
1105
+ detachRef(previous, fiber)
1106
+ fiber.ms = { ...fiber.ms, ref: fiber.ref }
1107
+ if (fiber.sn) attachRef(fiber, fiber.sn)
1108
+ }
1109
+ }
933
1110
  fiber.mp = props
934
1111
  // dy cleared at rerender start; leaving true lets mid-render schedule persist
935
1112
  }
@@ -960,11 +1137,35 @@ function defaultHandleSuspended(fiber: Fiber, thenable: Promise<any>): void {
960
1137
  export interface Capabilities {
961
1138
  handleSuspended: (fiber: Fiber, thenable: Promise<any>) => void
962
1139
  readContext: (fiber: Fiber, ctx: any) => any
1140
+ isActivityHidden: (fiber: Fiber) => boolean
1141
+ activityIsDisconnected: (fiber: Fiber) => boolean
1142
+ prepareActivityFiber: (fiber: Fiber) => void
1143
+ syncActivity: (fiber: Fiber) => void
963
1144
  }
964
1145
 
965
1146
  const CAPABILITIES: Capabilities = {
966
1147
  handleSuspended: defaultHandleSuspended,
967
1148
  readContext: defaultReadContext,
1149
+ isActivityHidden: () => false,
1150
+ activityIsDisconnected: () => false,
1151
+ prepareActivityFiber: () => {},
1152
+ syncActivity: () => {},
1153
+ }
1154
+
1155
+ export function isActivityHidden(fiber: Fiber): boolean {
1156
+ return !!(fiber.root ?? currentRoot ?? findRoot(fiber))?.a && CAPABILITIES.isActivityHidden(fiber)
1157
+ }
1158
+
1159
+ export function activityIsDisconnected(fiber: Fiber): boolean {
1160
+ return !!fiber.root?.a && CAPABILITIES.activityIsDisconnected(fiber)
1161
+ }
1162
+
1163
+ export function layoutIsDisconnected(fiber: Fiber): boolean {
1164
+ return !!fiber.ld || activityIsDisconnected(fiber)
1165
+ }
1166
+
1167
+ export function rememberActivityEffect(fiber: Fiber, hook: Hook, effect: Effect): void {
1168
+ if (fiber.root?.a || (fiber.root?.sp && effect.t === 1 && !effect.s)) rememberRetainedEffect(fiber, hook, effect)
968
1169
  }
969
1170
 
970
1171
  export function installCapability<K extends keyof Capabilities>(
@@ -977,35 +1178,119 @@ export function installCapability<K extends keyof Capabilities>(
977
1178
  // Wrapper for features that catch thrown thenables inside their render
978
1179
  // functions. Delegates to the installed Suspense capability.
979
1180
  export function handleSuspended(fiber: Fiber, thenable: Promise<any>): void {
1181
+ if (isActivityHidden(fiber) && !findSuspenseBoundary(fiber)) {
1182
+ defaultHandleSuspended(fiber, thenable)
1183
+ return
1184
+ }
980
1185
  CAPABILITIES.handleSuspended(fiber, thenable)
981
1186
  }
982
1187
 
1188
+ export class RenderSuspenseCapture {
1189
+ constructor(public boundary: Fiber) {}
1190
+ }
1191
+
1192
+ export function findSuspenseBoundary(fiber: Fiber): Fiber | null {
1193
+ let child = fiber
1194
+ for (let parent = fiber.parent; parent; parent = parent.parent) {
1195
+ if (parent.tag === FiberTag.Suspense && parent.ms?.f !== child) return parent
1196
+ // Hidden prerendering cannot activate a fallback outside its Activity.
1197
+ if (parent.tag === FiberTag.Activity && parent.pp?.mode === 'hidden') return null
1198
+ child = parent
1199
+ }
1200
+ return null
1201
+ }
1202
+
1203
+ export class RenderErrorCapture {
1204
+ stack: string
1205
+ constructor(public boundary: Fiber, public error: unknown, source: Fiber = boundary) {
1206
+ this.stack = componentStack(source)
1207
+ }
1208
+ }
1209
+
1210
+ function captureError(capture: RenderErrorCapture): void {
1211
+ const fiber = capture.boundary
1212
+ fiber.ms = { ...fiber.ms, error: capture }
1213
+ scheduleUpdate(fiber)
1214
+ }
1215
+
1216
+ function findErrorBoundary(fiber: Fiber): Fiber | null {
1217
+ for (let parent = fiber.parent; parent; parent = parent.parent) {
1218
+ if (parent.um || parent.pd) continue
1219
+ const instance = parent.sn
1220
+ if (instance?._fiber === parent && (parent.ms?.b?.getDerivedStateFromError || instance.componentDidCatch)) return parent
1221
+ }
1222
+ return null
1223
+ }
1224
+
1225
+ function captureRootError(root: FiberRoot, stack: string, error: unknown): void {
1226
+ const errors = root.er ||= []
1227
+ if (!errors.length) root.u = undefined
1228
+ errors.push({ error, stack })
1229
+ scheduleUpdate(root.r)
1230
+ }
1231
+
1232
+ function flushRootErrors(root: FiberRoot): void {
1233
+ const errors = root.er!
1234
+ root.er = undefined
1235
+ if (root.r.mp === null) queueMutation(() => { root.c.textContent = '' })
1236
+ renderRoot(root, null)
1237
+ queueCommitEffects(() => {
1238
+ for (const captured of errors) {
1239
+ try {
1240
+ if (root.ue) root.ue(captured.error, { componentStack: captured.stack })
1241
+ else if (typeof reportError === 'function') reportError(captured.error)
1242
+ else queueMicrotask(() => { throw captured.error })
1243
+ } catch (error) { setTimeout(() => { throw error }) }
1244
+ }
1245
+ })
1246
+ if (root.u) pendingRoots.add(root)
1247
+ }
1248
+
1249
+ export function recoverRootError(error: unknown, abort?: () => void): boolean {
1250
+ if (!(error instanceof RenderErrorCapture) || error.boundary.tag !== FiberTag.Root) return false
1251
+ const root = error.boundary.root!
1252
+ abort?.()
1253
+ discardPendingWork(root)
1254
+ discardPendingEffects(root)
1255
+ captureRootError(root, error.stack, error.error)
1256
+ return true
1257
+ }
1258
+
1259
+ export function handleCommitError(fiber: Fiber, error: unknown): void {
1260
+ const boundary = findErrorBoundary(fiber)
1261
+ if (boundary) {
1262
+ captureError(new RenderErrorCapture(boundary, error, fiber))
1263
+ return
1264
+ }
1265
+ const root = findRoot(fiber)
1266
+ if (root) captureRootError(root, componentStack(fiber), error)
1267
+ else queueMicrotask(() => { throw error })
1268
+ }
1269
+
983
1270
  export function handleErrorInRender(fiber: Fiber, err: any): void {
1271
+ if (err instanceof RenderErrorCapture) throw err
1272
+ removeStagedCommits(fiber)
1273
+ for (const queue of [pendingEffects, pendingInsertionEffects, pendingCommits]) {
1274
+ for (let index = queue.length - 1; index >= 0; index--) {
1275
+ if (queue[index]![0] === fiber) removePendingEffect(queue, index)
1276
+ }
1277
+ }
984
1278
  if (currentRoot?.h) {
985
1279
  abortHydration(err, fiber)
986
1280
  }
987
1281
  // Bubble to nearest class boundary with getDerivedStateFromError / componentDidCatch
988
- let f: Fiber | null = fiber.parent
989
- while (f) {
990
- if (f.tag === FiberTag.Class) {
991
- const Ctor = f.type as any
992
- const instance = f.sn
993
- if (Ctor.getDerivedStateFromError) {
994
- const update = Ctor.getDerivedStateFromError(err)
995
- instance.state = { ...instance.state, ...update }
996
- }
997
- if (instance.componentDidCatch) {
998
- try {
999
- instance.componentDidCatch(err, { componentStack: '' })
1000
- } catch {}
1001
- }
1002
- scheduleUpdate(f)
1003
- return
1004
- }
1005
- f = f.parent
1282
+ const boundary = findErrorBoundary(fiber)
1283
+ if (boundary) {
1284
+ const capture = new RenderErrorCapture(boundary, err, fiber)
1285
+ if (currentCommit) throw capture
1286
+ captureError(capture)
1287
+ return
1006
1288
  }
1007
- // No boundary report to root
1008
- if (currentRoot?.ue) currentRoot.ue(err)
1289
+ // Abort the prepared render. Its mutation/effect queues must not commit
1290
+ // before the root is cleared and the error callback runs.
1291
+ const root = currentRoot ?? findRoot(fiber)
1292
+ if (root && currentCommit && (flushing || root.ue)) throw new RenderErrorCapture(root.r, err, fiber)
1293
+ if (root?.ue) root.ue(err, { componentStack: componentStack(fiber) })
1009
1294
  else throw err
1010
1295
  }
1011
1296
 
@@ -1017,34 +1302,48 @@ export function isThenable(x: any): x is Promise<any> {
1017
1302
  // Unmount
1018
1303
  // ---------------------------------------------------------------------------
1019
1304
 
1020
- export function unmountFiber(fiber: Fiber, domParent: Node): void {
1305
+ export function unmountFiber(fiber: Fiber, domParent: Node, remove = true): void {
1306
+ if (fiber.um) return
1307
+ if (currentCommit) return queueUnmount(fiber, domParent, remove)
1308
+ removeStagedCommits(fiber)
1309
+ if (fiber.type === REACT_FRAGMENT_TYPE && fiber.sn) {
1310
+ if (fiber.cu) {
1311
+ for (const cleanup of fiber.cu) {
1312
+ try { cleanup() } catch (error) { handleCommitError(fiber, error) }
1313
+ }
1314
+ fiber.cu = null
1315
+ }
1316
+ detachRef(fiber.cr ?? fiber.ref, fiber)
1317
+ }
1021
1318
  fiber.um = true
1022
- // Recurse first
1023
- let c = fiber.child
1024
- while (c) {
1025
- const next = c.sibling
1026
- unmountFiber(c, fiber.tag === FiberTag.Host ? fiber.dom! : domParent)
1027
- c = next
1319
+ if (fiber.tag === FiberTag.Host || fiber.tag === FiberTag.Text) updateFragmentHost(fiber, false)
1320
+ if (fiber.type === REACT_FRAGMENT_TYPE && fiber.sn) fiber.sn.dispose()
1321
+ if (fiber.tag === FiberTag.Suspense && fiber.ms?.f) {
1322
+ unmountFiber(fiber.ms.f, domParent, remove)
1323
+ fiber.ms.f = null
1028
1324
  }
1029
- fiber.child = null
1030
-
1031
1325
  // Run cu (fx + layout fx)
1032
1326
  if (fiber.cu) {
1033
- for (const cleanup of fiber.cu) {
1327
+ const cleanups = fiber.cu
1328
+ fiber.cu = null
1329
+ for (const cleanup of cleanups) {
1034
1330
  try {
1035
1331
  cleanup()
1036
1332
  } catch (e) {
1037
- if (currentRoot?.re) currentRoot.re(e)
1333
+ handleCommitError(fiber, e)
1038
1334
  }
1039
1335
  }
1040
- fiber.cu = null
1041
1336
  }
1042
1337
 
1043
- if (fiber.tag === FiberTag.Class && fiber.sn?.componentWillUnmount) {
1044
- try {
1045
- fiber.sn.componentWillUnmount()
1046
- } catch (e) {
1047
- if (currentRoot?.re) currentRoot.re(e)
1338
+ if (fiber.sn?._fiber === fiber) {
1339
+ const committed = fiber.ms?.c
1340
+ if (committed) { fiber.sn.props = committed.props; fiber.sn.state = committed.state }
1341
+ if (fiber.sn.componentWillUnmount && !layoutIsDisconnected(fiber)) {
1342
+ try {
1343
+ fiber.sn.componentWillUnmount()
1344
+ } catch (e) {
1345
+ handleCommitError(fiber, e)
1346
+ }
1048
1347
  }
1049
1348
  fiber.sn._fiber = null
1050
1349
  fiber.sn._enqueueUpdate = null
@@ -1052,16 +1351,41 @@ export function unmountFiber(fiber: Fiber, domParent: Node): void {
1052
1351
  }
1053
1352
 
1054
1353
  // Detach ref
1055
- if (fiber.ref) detachRef(fiber.ref)
1354
+ if (fiber.cr ?? fiber.ref) detachRef(fiber.cr ?? fiber.ref, fiber)
1355
+
1356
+ let c = fiber.child
1357
+ while (c) {
1358
+ const next = c.sibling
1359
+ unmountFiber(c, fiber.tag === FiberTag.Host ? fiber.dom! : domParent, fiber.tag === FiberTag.Portal || (remove && fiber.tag !== FiberTag.Host))
1360
+ c = next
1361
+ }
1362
+ fiber.child = null
1056
1363
 
1057
1364
  // Remove DOM if host
1058
- if (fiber.tag === FiberTag.Host && fiber.dom && fiber.dom.parentNode) {
1059
- fiber.dom.parentNode.removeChild(fiber.dom)
1060
- } else if (fiber.tag === FiberTag.Text && fiber.dom && fiber.dom.parentNode) {
1365
+ if (remove && (fiber.tag === FiberTag.Host || fiber.tag === FiberTag.Text) && fiber.dom?.parentNode) {
1061
1366
  fiber.dom.parentNode.removeChild(fiber.dom)
1062
1367
  }
1063
1368
  }
1064
1369
 
1370
+ function queueUnmount(fiber: Fiber, domParent: Node, remove: boolean): void {
1371
+ if (fiber.pd) return
1372
+ // Keep committed DOM available for snapshots, but stop deleted work now.
1373
+ const deleted: Fiber[] = []
1374
+ markPendingDeletion(fiber, deleted)
1375
+ const restore = () => { for (const child of deleted) child.pd = false }
1376
+ onCommitRollback(restore)
1377
+ onCommitFailure(restore)
1378
+ queueMutation(() => unmountFiber(fiber, domParent, remove))
1379
+ }
1380
+
1381
+ function markPendingDeletion(fiber: Fiber, deleted: Fiber[]): void {
1382
+ if (fiber.pd) return
1383
+ fiber.pd = true
1384
+ deleted.push(fiber)
1385
+ for (let child = fiber.child; child; child = child.sibling) markPendingDeletion(child, deleted)
1386
+ if (fiber.tag === FiberTag.Suspense && fiber.ms?.f) markPendingDeletion(fiber.ms.f, deleted)
1387
+ }
1388
+
1065
1389
  export function unmountAllChildren(parent: Fiber, domParent: Node): void {
1066
1390
  let c = parent.child
1067
1391
  while (c) {
@@ -1077,6 +1401,7 @@ export function unmountAllChildren(parent: Fiber, domParent: Node): void {
1077
1401
  // ---------------------------------------------------------------------------
1078
1402
 
1079
1403
  function insertInto(parent: Node, node: Node, anchor: Node | null): void {
1404
+ if (currentCommit && parent !== preparedParent) return queueInsertion(parent, node, anchor)
1080
1405
  const projectedHeadParent = getDocumentHeadInsertionParent(parent, node)
1081
1406
  if (projectedHeadParent) {
1082
1407
  projectedHeadParent.appendChild(node)
@@ -1094,6 +1419,10 @@ function insertInto(parent: Node, node: Node, anchor: Node | null): void {
1094
1419
  }
1095
1420
  }
1096
1421
 
1422
+ function queueInsertion(parent: Node, node: Node, anchor: Node | null): void {
1423
+ queueMutation(() => insertInto(parent, node, anchor))
1424
+ }
1425
+
1097
1426
  const DOCUMENT_HEAD_TAGS = new Set(['base', 'link', 'meta', 'script', 'style', 'title'])
1098
1427
 
1099
1428
  function getDocumentHeadInsertionParent(parent: Node, node: Node): HTMLHeadElement | null {
@@ -1103,7 +1432,7 @@ function getDocumentHeadInsertionParent(parent: Node, node: Node): HTMLHeadEleme
1103
1432
  return (parent as Document).head
1104
1433
  }
1105
1434
 
1106
- function getHostParent(fiber: Fiber): Node {
1435
+ export function getHostParent(fiber: Fiber): Node {
1107
1436
  let p = fiber.parent
1108
1437
  while (p) {
1109
1438
  if (p.tag === FiberTag.Host) return p.dom!
@@ -1127,35 +1456,32 @@ function getHostParent(fiber: Fiber): Node {
1127
1456
  throw new Error()
1128
1457
  }
1129
1458
 
1130
- function getAnchor(fiber: Fiber): Node | null {
1131
- // Return the first DOM node that comes after this fiber within the host parent
1132
- let f: Fiber | null = fiber.sibling
1133
- while (f) {
1134
- const d = firstDomNode(f)
1135
- if (d) return d
1136
- f = f.sibling
1137
- }
1138
- // Ascend
1139
- let p = fiber.parent
1140
- while (p && p.tag !== FiberTag.Host && p.tag !== FiberTag.Root && p.tag !== FiberTag.Portal) {
1141
- if (p.sibling) {
1142
- const d = firstDomNode(p.sibling)
1459
+ export function getAnchor(fiber: Fiber, domParent: Node): Node | null {
1460
+ let p: Fiber | null = fiber
1461
+ while (p) {
1462
+ let sibling = p.sibling
1463
+ while (sibling) {
1464
+ const d = firstDomNode(sibling, domParent)
1143
1465
  if (d) return d
1466
+ sibling = sibling.sibling
1144
1467
  }
1145
1468
  p = p.parent
1469
+ if (p?.tag === FiberTag.Host || p?.tag === FiberTag.Root || p?.tag === FiberTag.Portal) break
1146
1470
  }
1147
1471
  return null
1148
1472
  }
1149
1473
 
1150
- function firstDomNode(fiber: Fiber): Node | null {
1151
- if (fiber.tag === FiberTag.Host || fiber.tag === FiberTag.Text) return fiber.dom
1474
+ function firstDomNode(fiber: Fiber, domParent: Node): Node | null {
1475
+ if (fiber.tag === FiberTag.Host || fiber.tag === FiberTag.Text) {
1476
+ return fiber.dom?.parentNode === domParent ? fiber.dom : null
1477
+ }
1152
1478
  let c = fiber.child
1153
1479
  while (c) {
1154
- const d = firstDomNode(c)
1480
+ const d = firstDomNode(c, domParent)
1155
1481
  if (d) return d
1156
1482
  c = c.sibling
1157
1483
  }
1158
- return null
1484
+ return fiber.tag === FiberTag.Suspense && fiber.ms?.f ? firstDomNode(fiber.ms.f, domParent) : null
1159
1485
  }
1160
1486
 
1161
1487
  // ---------------------------------------------------------------------------
@@ -1179,36 +1505,57 @@ function defaultReadContext(_fiber: Fiber, ctx: any): any {
1179
1505
  // Refs
1180
1506
  // ---------------------------------------------------------------------------
1181
1507
 
1182
- function attachRef(fiber: Fiber, value: any): void {
1508
+ export function attachRef(fiber: Fiber, value: any): void {
1183
1509
  const ref = fiber.ref ?? (fiber.pp?.ref ?? null)
1184
1510
  if (!ref) return
1185
- if (typeof ref == 'function') {
1186
- // Match React's commit-phase semantics: callback refs run after render
1187
- // (during the layout/commit phase), not during render. Calling them
1188
- // synchronously here breaks libraries that assert no event handlers run
1189
- // during render (e.g. base-ui's useStableCallback trampoline).
1190
- scheduleLifecycle(fiber, () => {
1511
+ if (isActivityHidden(fiber) || layoutIsDisconnected(fiber)) return
1512
+ stageCommit(fiber, () => {
1513
+ if (fiber.tag === FiberTag.Resource) value = fiber.sn
1514
+ fiber.cr = ref
1515
+ if (typeof ref == 'function') {
1516
+ // Callback refs run during commit, after insertion effects install
1517
+ // event callback implementations and in subtree layout order.
1191
1518
  const cleanup = ref(value)
1192
1519
  fiber.cu ||= []
1193
- fiber.cu.push(typeof cleanup == 'function' ? cleanup : () => ref(null))
1194
- })
1195
- } else {
1196
- ref.current = value
1197
- }
1520
+ const detach = typeof cleanup == 'function' ? cleanup : () => ref(null)
1521
+ fiber.rc = detach
1522
+ fiber.cu.push(detach)
1523
+ } else {
1524
+ ref.current = value
1525
+ }
1526
+ }, true)
1198
1527
  }
1199
1528
 
1200
- function syncRefIfChanged(fiber: Fiber, value: any): void {
1529
+ export function syncRefIfChanged(fiber: Fiber, value: any): void {
1201
1530
  const ref = fiber.ref ?? (fiber.pp?.ref ?? null)
1202
- if (!ref) return
1203
- if (typeof ref === 'object' && ref.current !== value) ref.current = value
1531
+ if (!ref && !fiber.cr) return
1532
+ if (isActivityHidden(fiber) || layoutIsDisconnected(fiber)) return
1533
+ const previous = fiber.cr ?? null
1534
+ if (previous !== ref) {
1535
+ const cleanup = fiber.rc
1536
+ queueMutation(() => {
1537
+ if (cleanup) {
1538
+ const index = fiber.cu?.indexOf(cleanup) ?? -1
1539
+ if (index >= 0) fiber.cu!.splice(index, 1)
1540
+ fiber.rc = null
1541
+ try { cleanup() } catch (error) { handleCommitError(fiber, error) }
1542
+ }
1543
+ detachRef(previous, fiber)
1544
+ fiber.cr = null
1545
+ })
1546
+ attachRef(fiber, value)
1547
+ } else if (ref && typeof ref === 'object' && ref.current !== value) attachRef(fiber, value)
1204
1548
  }
1205
1549
 
1206
- function detachRef(ref: any): void {
1550
+ export function detachRef(ref: any, fiber?: Fiber): void {
1207
1551
  // Function refs are handled via fiber.cu (queued in attachRef during
1208
1552
  // the commit phase): the cleanup either invokes the user-returned cleanup
1209
1553
  // fn or calls ref(null). Calling ref(null) here would double-fire it.
1210
1554
  if (ref && typeof ref === 'object') {
1211
- ref.current = null
1555
+ queueMutation(() => {
1556
+ try { ref.current = null }
1557
+ catch (error) { if (fiber) handleCommitError(fiber, error); else throw error }
1558
+ })
1212
1559
  }
1213
1560
  }
1214
1561
 
@@ -1217,46 +1564,146 @@ function detachRef(ref: any): void {
1217
1564
  // ---------------------------------------------------------------------------
1218
1565
 
1219
1566
  const pendingEffects: Array<[Fiber, Effect]> = []
1220
- const pendingLayoutEffects: Array<[Fiber, Effect]> = []
1221
- const pendingLifecycles: Array<() => void> = []
1567
+ const pendingInsertionEffects: Array<[Fiber, Effect]> = []
1568
+ type LayoutCommit = [Fiber, Effect | (() => void), boolean]
1569
+ const pendingCommits: LayoutCommit[] = []
1570
+ const stagedCommits = new Map<Fiber, LayoutCommit[]>()
1571
+ // Track only fibers with queued work. A ref-less descendant can finish with
1572
+ // one identity check, even while an ancestor has a pending layout effect.
1573
+ const stagedFibers: Fiber[] = []
1574
+ let lastStagedFiber: Fiber | undefined
1575
+
1576
+ // Appends are covered by checkpoint lengths. Only removals need to retain
1577
+ // the affected entry, and only while a render has a rollback checkpoint.
1578
+ function removePendingEffect(queue: Array<[Fiber, Effect] | LayoutCommit>, index: number): void {
1579
+ const entry = queue[index]!
1580
+ queue.splice(index, 1)
1581
+ if (currentCommit?.c.length) onCommitRollback(() => { queue.splice(index, 0, entry) })
1582
+ }
1222
1583
 
1223
- export function enqueueEffect(fiber: Fiber, effect: Effect): void {
1224
- if (effect.t) {
1225
- pendingLayoutEffects.push([fiber, effect])
1584
+ function stageCommit(fiber: Fiber, work: Effect | (() => void), ref = false): void {
1585
+ let entries = stagedCommits.get(fiber)
1586
+ if (!entries) {
1587
+ stagedCommits.set(fiber, entries = [])
1588
+ stagedFibers.push(fiber)
1589
+ lastStagedFiber = fiber
1590
+ }
1591
+ entries.push([fiber, work, ref])
1592
+ }
1593
+
1594
+ function removeStagedCommits(fiber: Fiber): void {
1595
+ if (!stagedCommits.delete(fiber)) return
1596
+ if (lastStagedFiber === fiber) stagedFibers.pop()
1597
+ else stagedFibers.splice(stagedFibers.lastIndexOf(fiber), 1)
1598
+ lastStagedFiber = stagedFibers[stagedFibers.length - 1]
1599
+ }
1600
+
1601
+ // Rendering already visits the tree in order. Completing a fiber stages its
1602
+ // layout work after its children, without another tree walk at commit time.
1603
+ export function flushFiberCommits(fiber: Fiber): void {
1604
+ const entries = stagedCommits.get(fiber)
1605
+ if (!entries) return
1606
+ removeStagedCommits(fiber)
1607
+ if (fiber.type === REACT_FRAGMENT_TYPE) {
1608
+ // React attaches Fragment refs before their descendants. Only referenced
1609
+ // Fragments need to locate this insertion point among queued commits.
1610
+ let index = pendingCommits.length
1611
+ outer: for (let i = 0; i < pendingCommits.length; i++) {
1612
+ for (let parent = pendingCommits[i]![0].parent; parent; parent = parent.parent) {
1613
+ if (parent === fiber) { index = i; break outer }
1614
+ }
1615
+ }
1616
+ if (index < pendingCommits.length && currentCommit?.c.length) {
1617
+ const count = entries.length
1618
+ onCommitRollback(() => { pendingCommits.splice(index, count) })
1619
+ }
1620
+ pendingCommits.splice(index, 0, ...entries)
1621
+ } else if (fiber.tag === FiberTag.Class || fiber.sn?._fiber === fiber) {
1622
+ for (const entry of entries) if (!entry[2]) pendingCommits.push(entry)
1623
+ for (const entry of entries) if (entry[2]) pendingCommits.push(entry)
1226
1624
  } else {
1227
- pendingEffects.push([fiber, effect])
1625
+ for (const entry of entries) pendingCommits.push(entry)
1228
1626
  }
1229
1627
  }
1230
1628
 
1231
- export function scheduleLifecycle(_fiber: Fiber, fn: () => void): void {
1232
- pendingLifecycles.push(fn)
1629
+ export function enqueueEffect(fiber: Fiber, effect: Effect): void {
1630
+ if (effect.t !== 2 && (isActivityHidden(fiber) || activityIsDisconnected(fiber) || (effect.t === 1 && !effect.s && fiber.ld))) return
1631
+ if (effect.t === 2) pendingInsertionEffects.push([fiber, effect])
1632
+ else if (effect.t === 1) stageCommit(fiber, effect)
1633
+ else pendingEffects.push([fiber, effect])
1634
+ }
1635
+
1636
+ export function scheduleLifecycle(fiber: Fiber, fn: () => void): void {
1637
+ if (isActivityHidden(fiber) || layoutIsDisconnected(fiber)) return
1638
+ stageCommit(fiber, fn)
1233
1639
  }
1234
1640
 
1235
1641
  export function runEffects(root: FiberRoot): void {
1236
- // Layout fx synchronously
1237
- while (pendingLayoutEffects.length) {
1238
- const [fiber, effect] = pendingLayoutEffects.shift()!
1239
- runEffect(fiber, effect, root)
1240
- }
1241
- // Then lifecycles
1242
- while (pendingLifecycles.length) {
1243
- const fn = pendingLifecycles.shift()!
1244
- try {
1245
- fn()
1246
- } catch (e) {
1247
- if (root.ce) root.ce(e)
1642
+ if (root.fr) refreshFragmentInstances(root)
1643
+ if (!pendingInsertionEffects.length && !pendingCommits.length && !pendingEffects.length) return
1644
+ flushEffects(root)
1645
+ }
1646
+
1647
+ function flushEffects(root: FiberRoot): void {
1648
+ const insertion = pendingInsertionEffects.splice(0)
1649
+ const layout = pendingCommits.splice(0)
1650
+ const batch = pendingEffects.splice(0)
1651
+ if (currentCommit?.c.length) onCommitRollback(() => {
1652
+ // A nested render can drain these queues before an enclosing attempt
1653
+ // fails. Reuse the captured batches, then older checkpoints trim tails.
1654
+ pendingInsertionEffects.unshift(...insertion)
1655
+ pendingCommits.unshift(...layout)
1656
+ pendingEffects.unshift(...batch)
1657
+ })
1658
+ queueCommitEffects(() => {
1659
+ for (const [fiber, effect] of insertion) runEffect(fiber, effect, root)
1660
+ if (layout.length) {
1661
+ for (const [fiber, work] of layout) {
1662
+ if (typeof work !== 'function' && !fiber.um && !isActivityHidden(fiber) && !layoutIsDisconnected(fiber)) work.d?.()
1248
1663
  }
1664
+ const runLayout = () => {
1665
+ for (const [fiber, work] of layout) {
1666
+ if (typeof work !== 'function') runEffect(fiber, work, root)
1667
+ else if (!fiber.um && !isActivityHidden(fiber) && !layoutIsDisconnected(fiber)) {
1668
+ try { work() } catch (error) { handleCommitError(fiber, error) }
1669
+ }
1670
+ }
1671
+ }
1672
+ if (!deferTransitionLayout(runLayout)) runLayout()
1249
1673
  }
1250
1674
  // Passive fx on microtask
1251
- if (pendingEffects.length) {
1252
- const batch = pendingEffects.splice(0)
1253
- queueMicrotask(() => {
1675
+ if (batch.length) {
1676
+ const passive = () => {
1254
1677
  for (const [fiber, effect] of batch) runEffect(fiber, effect, root)
1255
- })
1678
+ }
1679
+ if (!deferTransitionPassive(passive)) queueMicrotask(passive)
1256
1680
  }
1681
+ })
1257
1682
  }
1258
1683
 
1684
+ setCommitCheckpointHook(() => {
1685
+ // A parent allocated before this savepoint can contain eager insertions
1686
+ // from an abandoned child render. Restore only that new, detached host.
1687
+ const parent = preparedParent as Element | null
1688
+ const children = parent && Array.from(parent.childNodes)
1689
+ const effects = pendingEffects.length, insertion = pendingInsertionEffects.length, commits = pendingCommits.length
1690
+ const staged = new Map([...stagedCommits].map(([fiber, work]) => [fiber, work.slice()]))
1691
+ const stack = stagedFibers.slice(), last = lastStagedFiber
1692
+ onCommitRollback(() => {
1693
+ if (parent) parent.replaceChildren(...children!)
1694
+ pendingEffects.length = effects
1695
+ pendingInsertionEffects.length = insertion
1696
+ pendingCommits.length = commits
1697
+ stagedCommits.clear()
1698
+ for (const [fiber, work] of staged) stagedCommits.set(fiber, work)
1699
+ stagedFibers.splice(0, stagedFibers.length, ...stack)
1700
+ lastStagedFiber = last
1701
+ })
1702
+ })
1703
+
1259
1704
  function runEffect(fiber: Fiber, effect: Effect, root: FiberRoot): void {
1705
+ if (fiber.um) return
1706
+ if (effect.t !== 2 && (isActivityHidden(fiber) || activityIsDisconnected(fiber) || (effect.t === 1 && !effect.s && fiber.ld))) return
1260
1707
  try {
1261
1708
  const cleanup = effect.c()
1262
1709
  if (typeof cleanup == 'function') {
@@ -1264,7 +1711,7 @@ function runEffect(fiber: Fiber, effect: Effect, root: FiberRoot): void {
1264
1711
  fiber.cu.push(cleanup)
1265
1712
  }
1266
1713
  } catch (e) {
1267
- if (root.ce) root.ce(e)
1714
+ handleCommitError(fiber, e)
1268
1715
  }
1269
1716
  }
1270
1717