@reckona/mreact-compat 0.0.91 → 0.0.93

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. package/README.md +1 -0
  2. package/dist/class-component.d.ts +22 -6
  3. package/dist/class-component.d.ts.map +1 -1
  4. package/dist/class-component.js +157 -51
  5. package/dist/class-component.js.map +1 -1
  6. package/dist/context.d.ts +19 -3
  7. package/dist/context.d.ts.map +1 -1
  8. package/dist/context.js +55 -6
  9. package/dist/context.js.map +1 -1
  10. package/dist/dom-children.d.ts +2 -0
  11. package/dist/dom-children.d.ts.map +1 -1
  12. package/dist/dom-children.js +103 -1
  13. package/dist/dom-children.js.map +1 -1
  14. package/dist/dom-host-rules.d.ts +10 -0
  15. package/dist/dom-host-rules.d.ts.map +1 -0
  16. package/dist/dom-host-rules.js +86 -0
  17. package/dist/dom-host-rules.js.map +1 -0
  18. package/dist/dom-props.d.ts +3 -2
  19. package/dist/dom-props.d.ts.map +1 -1
  20. package/dist/dom-props.js +229 -33
  21. package/dist/dom-props.js.map +1 -1
  22. package/dist/element.d.ts +9 -4
  23. package/dist/element.d.ts.map +1 -1
  24. package/dist/element.js +101 -26
  25. package/dist/element.js.map +1 -1
  26. package/dist/event-listeners.d.ts +4 -4
  27. package/dist/event-listeners.d.ts.map +1 -1
  28. package/dist/event-listeners.js +1 -1
  29. package/dist/event-listeners.js.map +1 -1
  30. package/dist/event-types.d.ts +10 -0
  31. package/dist/event-types.d.ts.map +1 -1
  32. package/dist/event-types.js.map +1 -1
  33. package/dist/events.js +22 -1
  34. package/dist/events.js.map +1 -1
  35. package/dist/fiber-commit.d.ts +2 -1
  36. package/dist/fiber-commit.d.ts.map +1 -1
  37. package/dist/fiber-commit.js +13 -1
  38. package/dist/fiber-commit.js.map +1 -1
  39. package/dist/fiber-reconciler.d.ts.map +1 -1
  40. package/dist/fiber-reconciler.js +28 -7
  41. package/dist/fiber-reconciler.js.map +1 -1
  42. package/dist/fiber-work-loop.d.ts.map +1 -1
  43. package/dist/fiber-work-loop.js +4 -3
  44. package/dist/fiber-work-loop.js.map +1 -1
  45. package/dist/fiber.d.ts +5 -0
  46. package/dist/fiber.d.ts.map +1 -1
  47. package/dist/fiber.js +9 -0
  48. package/dist/fiber.js.map +1 -1
  49. package/dist/hooks-entry.d.ts +3 -0
  50. package/dist/hooks-entry.d.ts.map +1 -0
  51. package/dist/hooks-entry.js +2 -0
  52. package/dist/hooks-entry.js.map +1 -0
  53. package/dist/hooks.d.ts +39 -5
  54. package/dist/hooks.d.ts.map +1 -1
  55. package/dist/hooks.js +373 -326
  56. package/dist/hooks.js.map +1 -1
  57. package/dist/host-reconciler.d.ts +3 -0
  58. package/dist/host-reconciler.d.ts.map +1 -1
  59. package/dist/host-reconciler.js +1183 -68
  60. package/dist/host-reconciler.js.map +1 -1
  61. package/dist/hydration.d.ts +1 -1
  62. package/dist/hydration.d.ts.map +1 -1
  63. package/dist/hydration.js.map +1 -1
  64. package/dist/index.d.ts +2 -1
  65. package/dist/index.d.ts.map +1 -1
  66. package/dist/index.js +2 -1
  67. package/dist/index.js.map +1 -1
  68. package/dist/react-default.d.ts +4 -4
  69. package/dist/react-default.d.ts.map +1 -1
  70. package/dist/react-default.js +2 -1
  71. package/dist/react-default.js.map +1 -1
  72. package/dist/reconciler.d.ts.map +1 -1
  73. package/dist/reconciler.js +38 -22
  74. package/dist/reconciler.js.map +1 -1
  75. package/dist/root.d.ts.map +1 -1
  76. package/dist/root.js +48 -13
  77. package/dist/root.js.map +1 -1
  78. package/dist/server-render.d.ts +6 -0
  79. package/dist/server-render.d.ts.map +1 -0
  80. package/dist/server-render.js +307 -0
  81. package/dist/server-render.js.map +1 -0
  82. package/package.json +6 -2
  83. package/src/class-component.ts +313 -51
  84. package/src/context.ts +108 -9
  85. package/src/dom-children.ts +155 -1
  86. package/src/dom-host-rules.ts +115 -0
  87. package/src/dom-props.ts +297 -46
  88. package/src/element.ts +141 -31
  89. package/src/event-listeners.ts +6 -6
  90. package/src/event-types.ts +10 -0
  91. package/src/events.ts +32 -10
  92. package/src/fiber-commit.ts +16 -1
  93. package/src/fiber-reconciler.ts +39 -6
  94. package/src/fiber-work-loop.ts +4 -3
  95. package/src/fiber.ts +14 -0
  96. package/src/hooks-entry.ts +24 -0
  97. package/src/hooks.ts +482 -479
  98. package/src/host-reconciler.ts +1662 -83
  99. package/src/hydration.ts +1 -1
  100. package/src/index.ts +1 -1
  101. package/src/react-default.ts +1 -1
  102. package/src/reconciler.ts +61 -22
  103. package/src/root.ts +55 -12
  104. package/src/server-render.ts +478 -0
package/src/hooks.ts CHANGED
@@ -1,26 +1,13 @@
1
1
  import { scheduleCallback } from "./fiber-scheduler.js";
2
+ import { removeChildIfPresent } from "./dom-children.js";
2
3
  import {
3
- Activity,
4
- Fragment,
5
- FORWARD_REF_TYPE,
6
- MEMO_TYPE,
7
- Profiler,
8
- isReactCompatElement,
9
- type ForwardRefType,
10
- type MemoType,
11
- type ReactCompatElement,
12
- type ReactCompatNode,
13
- } from "./element.js";
14
- import {
4
+ type ReactCompatContextLike,
15
5
  isReactCompatContext,
16
- isReactCompatConsumer,
17
- isReactCompatProvider,
18
- renderWithContextProvider,
6
+ readContextValue,
19
7
  useContext,
8
+ withContextReadObserver,
20
9
  } from "./context.js";
21
10
  import { isThenable } from "./thenable.js";
22
- import { isDangerousHtmlAttribute, isDangerousHtmlOptIn } from "./url-safety.js";
23
- import { escapeHtmlAttribute as escapeHtml } from "@reckona/mreact-shared/html-escape";
24
11
 
25
12
  export interface RootRuntime {
26
13
  currentElement?: unknown;
@@ -35,11 +22,19 @@ export interface RootRuntime {
35
22
  pendingEffects: PendingEffect[];
36
23
  externalStoreChecks: ExternalStoreCheck[];
37
24
  portalContainers: Set<Element>;
25
+ portalNodes: Map<Element, Set<Node>>;
38
26
  idCounter: number;
39
27
  identifierPrefix: string;
40
28
  idMode: "client" | "server";
41
29
  strictModeDepth: number;
30
+ strictReplayDepth: number;
31
+ strictMemoCapture: unknown[] | undefined;
32
+ strictMemoCaptureByHook: Map<string, unknown> | undefined;
33
+ strictMemoReplay: { values: readonly unknown[]; index: number } | undefined;
34
+ strictMemoReplayByHook: ReadonlyMap<string, unknown> | undefined;
42
35
  profilerFlushDepth: number;
36
+ effectFlushPhase: "insertion" | "layout" | "normal" | undefined;
37
+ externalStoreUpdate: boolean;
43
38
  rerender(priority?: RenderPriority): void;
44
39
  beginRender(): void;
45
40
  endRender(committed?: boolean): void;
@@ -48,9 +43,13 @@ export interface RootRuntime {
48
43
  }
49
44
 
50
45
  interface ComponentInstance {
46
+ owner: unknown | undefined;
47
+ path: string;
51
48
  hooks: HookSlot[];
52
49
  hookIndex: number;
53
50
  dirty: boolean;
51
+ disposed?: boolean;
52
+ contextDependencies?: Map<ReactCompatContextLike<unknown>, unknown>;
54
53
  devToolsHooks: DevToolsHookValue[];
55
54
  devToolsHookTypes: string[];
56
55
  devToolsHookSuppressionDepth: number;
@@ -119,7 +118,7 @@ export type DevToolsHookValue =
119
118
  | { kind: "effect"; effectKind: "insertion" | "layout" | "normal"; deps?: readonly unknown[] };
120
119
 
121
120
  type HookSlot =
122
- | { kind: "state"; value: unknown }
121
+ | { kind: "state"; value: unknown; hostCommitValue?: unknown }
123
122
  | {
124
123
  kind: "action-state";
125
124
  state: unknown;
@@ -135,7 +134,7 @@ type HookSlot =
135
134
  update: (state: unknown, payload: unknown) => unknown;
136
135
  dispatch?: (payload: unknown) => void;
137
136
  }
138
- | { kind: "store"; value: unknown }
137
+ | { kind: "store"; value: unknown; hasMounted?: boolean }
139
138
  | { kind: "ref"; value: { current: unknown } }
140
139
  | { kind: "memo"; value: unknown; deps?: readonly unknown[] }
141
140
  | { kind: "debug"; value: unknown }
@@ -146,12 +145,31 @@ type HookSlot =
146
145
  deps?: readonly unknown[];
147
146
  cleanup?: () => void;
148
147
  disposed?: boolean;
148
+ mounted?: boolean;
149
149
  strictReplay?: boolean;
150
150
  };
151
151
 
152
- let currentRuntime: RootRuntime | undefined;
153
- let currentInstance: ComponentInstance | undefined;
154
- let currentCacheScope: CacheScope | undefined;
152
+ interface HookRenderState {
153
+ currentRuntime: RootRuntime | undefined;
154
+ currentInstance: ComponentInstance | undefined;
155
+ currentCacheScope: CacheScope | undefined;
156
+ hostCommitDepth: number;
157
+ queuedHostCommitRerenders: Set<RootRuntime>;
158
+ queuedEffectFlushRerenders: Set<RootRuntime>;
159
+ }
160
+
161
+ const HOOK_RENDER_STATE_KEY = Symbol.for("modular.react.hook_render_state");
162
+ const hookRenderState =
163
+ ((globalThis as typeof globalThis & Record<symbol, HookRenderState | undefined>)[
164
+ HOOK_RENDER_STATE_KEY
165
+ ] ??= {
166
+ currentRuntime: undefined,
167
+ currentInstance: undefined,
168
+ currentCacheScope: undefined,
169
+ hostCommitDepth: 0,
170
+ queuedHostCommitRerenders: new Set<RootRuntime>(),
171
+ queuedEffectFlushRerenders: new Set<RootRuntime>(),
172
+ });
155
173
  const CACHE_SCOPE_SYMBOL = Symbol.for("modular.react.cache_scope");
156
174
  const emptyCacheOwnerStack: string[] = [];
157
175
  let syncVersion = 0;
@@ -162,18 +180,77 @@ let transitionRerenderScheduled = false;
162
180
  let eventBatchDepth = 0;
163
181
  let currentEventPriority: EventPriority = "default";
164
182
  let eventRerenderScheduled = false;
183
+ let effectFlushRerenderDepth = 0;
184
+ let strictMemoOwnerId = 0;
185
+ const strictMemoObjectOwnerIds = new WeakMap<object, number>();
186
+ const strictMemoPrimitiveOwnerIds = new Map<unknown, number>();
165
187
  const queuedTransitionRerenders = new Map<RootRuntime, TransitionContext>();
166
188
  const queuedEventRerenders = new Set<RootRuntime>();
167
189
  export const version = "19.2.6";
168
190
 
169
191
  export function act<T>(callback: () => T): T extends PromiseLike<unknown> ? Promise<void> : void {
170
- const result = callback();
192
+ const previousPriority = currentEventPriority;
193
+ currentEventPriority = "discrete";
194
+ eventBatchDepth += 1;
195
+ let result: T;
196
+
197
+ try {
198
+ result = callback();
199
+ } catch (error) {
200
+ eventBatchDepth -= 1;
201
+ currentEventPriority = previousPriority;
202
+ if (eventBatchDepth === 0) {
203
+ flushEventRerendersForPriority("discrete");
204
+ }
205
+ throw error;
206
+ }
207
+
208
+ const finishActScope = (): void => {
209
+ eventBatchDepth -= 1;
210
+ currentEventPriority = previousPriority;
211
+ if (eventBatchDepth === 0) {
212
+ flushEventRerendersForPriority("discrete");
213
+ }
214
+ };
215
+
216
+ if (isThenable(result)) {
217
+ return Promise.resolve(result).then(
218
+ async () => {
219
+ finishActScope();
220
+ await flushActWorkAsync();
221
+ },
222
+ (error: unknown) => {
223
+ finishActScope();
224
+ throw error;
225
+ },
226
+ ) as T extends PromiseLike<unknown> ? Promise<void> : void;
227
+ }
171
228
 
172
- return (isThenable(result) ? Promise.resolve(result).then(() => undefined) : undefined) as T extends PromiseLike<unknown>
229
+ finishActScope();
230
+ flushActWork();
231
+ return undefined as T extends PromiseLike<unknown>
173
232
  ? Promise<void>
174
233
  : void;
175
234
  }
176
235
 
236
+ async function flushActWorkAsync(): Promise<void> {
237
+ for (let attempt = 0; attempt < 10; attempt += 1) {
238
+ await Promise.resolve();
239
+ flushActWork();
240
+ }
241
+ for (let attempt = 0; attempt < 3; attempt += 1) {
242
+ await new Promise((resolve) => setTimeout(resolve, 0));
243
+ flushActWork();
244
+ }
245
+ }
246
+
247
+ function flushActWork(): void {
248
+ flushQueuedEventRerenders("sync");
249
+ flushQueuedTransitionRerenders();
250
+ flushHostCommitRerenders();
251
+ flushEffectFlushRerenders();
252
+ }
253
+
177
254
  export type EventPriority = "discrete" | "continuous" | "default";
178
255
  export type RenderPriority = "sync" | "transition" | "continuous";
179
256
 
@@ -190,6 +267,7 @@ export interface RootRuntimeOptions {
190
267
  export interface RuntimeSnapshot {
191
268
  instanceKeys: Set<string>;
192
269
  portalContainers: Set<Element>;
270
+ portalNodes: Map<Element, Set<Node>>;
193
271
  pendingInsertionEffectsLength: number;
194
272
  pendingLayoutEffectsLength: number;
195
273
  pendingEffectsLength: number;
@@ -199,6 +277,11 @@ export interface RuntimeSnapshot {
199
277
  identifierPrefix: string;
200
278
  idMode: "client" | "server";
201
279
  strictModeDepth: number;
280
+ strictReplayDepth: number;
281
+ strictMemoCapture: unknown[] | undefined;
282
+ strictMemoCaptureByHook: Map<string, unknown> | undefined;
283
+ strictMemoReplay: { values: readonly unknown[]; index: number } | undefined;
284
+ strictMemoReplayByHook: ReadonlyMap<string, unknown> | undefined;
202
285
  profilerFlushDepth: number;
203
286
  }
204
287
 
@@ -218,11 +301,19 @@ export function createRootRuntime(
218
301
  pendingEffects: [],
219
302
  externalStoreChecks: [],
220
303
  portalContainers: new Set(),
304
+ portalNodes: new Map(),
221
305
  idCounter: 0,
222
306
  identifierPrefix: options.identifierPrefix ?? "",
223
307
  idMode: options.idMode ?? "client",
224
308
  strictModeDepth: 0,
309
+ strictReplayDepth: 0,
310
+ strictMemoCapture: undefined,
311
+ strictMemoCaptureByHook: undefined,
312
+ strictMemoReplay: undefined,
313
+ strictMemoReplayByHook: undefined,
225
314
  profilerFlushDepth: 0,
315
+ effectFlushPhase: undefined,
316
+ externalStoreUpdate: false,
226
317
  rerender,
227
318
  beginRender() {
228
319
  this.activeInstanceKeys = new Set();
@@ -245,23 +336,33 @@ export function createRootRuntime(
245
336
  }
246
337
  this.activeInstanceKeys = undefined;
247
338
  this.activeProfilerPaths = undefined;
248
- currentRuntime = undefined;
249
- currentInstance = undefined;
339
+ hookRenderState.currentRuntime = undefined;
340
+ hookRenderState.currentInstance = undefined;
250
341
  if (committed) {
251
342
  flushProfilerCommits(this, profilerCommits);
343
+ flushHostCommitRerenders();
344
+ this.externalStoreUpdate = false;
252
345
  }
253
346
  },
254
347
  flushEffects() {
255
348
  this.profilerFlushDepth += 1;
256
349
  try {
350
+ this.effectFlushPhase = "insertion";
257
351
  flushPendingEffects(this.pendingInsertionEffects);
352
+ this.effectFlushPhase = "layout";
258
353
  const strictLayoutEffects = flushPendingEffects(this.pendingLayoutEffects);
354
+ this.effectFlushPhase = "normal";
259
355
  const strictEffects = flushPendingEffects(this.pendingEffects);
356
+ this.effectFlushPhase = undefined;
260
357
  const strictReplayEffects = [...strictLayoutEffects, ...strictEffects];
261
358
  cleanupStrictEffects(strictReplayEffects);
262
359
  replayStrictEffects(strictReplayEffects);
263
360
  } finally {
361
+ this.effectFlushPhase = undefined;
264
362
  this.profilerFlushDepth -= 1;
363
+ if (this.profilerFlushDepth === 0) {
364
+ flushEffectFlushRerenders();
365
+ }
265
366
  }
266
367
  },
267
368
  dispose() {
@@ -276,10 +377,7 @@ export function createRootRuntime(
276
377
  this.activeProfilerPaths = undefined;
277
378
  this.mountedProfilerPaths.clear();
278
379
  this.profilerBaseDurations.clear();
279
- for (const container of this.portalContainers) {
280
- container.replaceChildren();
281
- }
282
- this.portalContainers.clear();
380
+ clearRuntimePortalNodes(this);
283
381
  },
284
382
  };
285
383
  }
@@ -299,9 +397,9 @@ export function refreshCacheScope(scope: CacheScope): void {
299
397
  }
300
398
 
301
399
  export function runWithCacheScope<T>(scope: CacheScope, callback: () => T): T {
302
- const previousScope = currentCacheScope;
400
+ const previousScope = hookRenderState.currentCacheScope;
303
401
  const previousGlobalScope = getGlobalCacheScope();
304
- currentCacheScope = scope;
402
+ hookRenderState.currentCacheScope = scope;
305
403
  setGlobalCacheScope(scope);
306
404
 
307
405
  try {
@@ -309,16 +407,16 @@ export function runWithCacheScope<T>(scope: CacheScope, callback: () => T): T {
309
407
 
310
408
  if (isThenable(result)) {
311
409
  return Promise.resolve(result).finally(() => {
312
- currentCacheScope = previousScope;
410
+ hookRenderState.currentCacheScope = previousScope;
313
411
  setGlobalCacheScope(previousGlobalScope);
314
412
  }) as T;
315
413
  }
316
414
 
317
- currentCacheScope = previousScope;
415
+ hookRenderState.currentCacheScope = previousScope;
318
416
  setGlobalCacheScope(previousGlobalScope);
319
417
  return result;
320
418
  } catch (error) {
321
- currentCacheScope = previousScope;
419
+ hookRenderState.currentCacheScope = previousScope;
322
420
  setGlobalCacheScope(previousGlobalScope);
323
421
  throw error;
324
422
  }
@@ -332,7 +430,7 @@ export function renderWithProfiler<T>(
332
430
  ): T {
333
431
  const startTime = getCurrentTime();
334
432
  const phase: ProfilerPhase =
335
- runtime.profilerFlushDepth > 0
433
+ runtime.profilerFlushDepth > 0 || effectFlushRerenderDepth > 0
336
434
  ? "nested-update"
337
435
  : runtime.mountedProfilerPaths.has(path)
338
436
  ? "update"
@@ -368,10 +466,20 @@ export function renderWithRootRuntime<T>(
368
466
  runtime: RootRuntime,
369
467
  path: string,
370
468
  render: () => T,
469
+ owner?: unknown,
371
470
  ): T {
372
- const previousRuntime = currentRuntime;
373
- const previousInstance = currentInstance;
374
- const instance = runtime.instances.get(path) ?? {
471
+ const previousRuntime = hookRenderState.currentRuntime;
472
+ const previousInstance = hookRenderState.currentInstance;
473
+ let instance = runtime.instances.get(path);
474
+
475
+ if (instance !== undefined && owner !== undefined && instance.owner !== owner) {
476
+ cleanupInstance(instance);
477
+ instance = undefined;
478
+ }
479
+
480
+ instance ??= {
481
+ owner,
482
+ path,
375
483
  hooks: [],
376
484
  hookIndex: 0,
377
485
  dirty: false,
@@ -379,22 +487,56 @@ export function renderWithRootRuntime<T>(
379
487
  devToolsHookTypes: [],
380
488
  devToolsHookSuppressionDepth: 0,
381
489
  };
490
+ instance.owner = owner;
491
+ instance.path = path;
382
492
  runtime.instances.set(path, instance);
383
493
  runtime.activeInstanceKeys?.add(path);
384
494
  instance.hookIndex = 0;
385
495
  instance.dirty = false;
496
+ instance.disposed = false;
497
+ delete instance.contextDependencies;
386
498
  instance.devToolsHooks = [];
387
499
  instance.devToolsHookTypes = [];
388
500
  instance.devToolsHookSuppressionDepth = 0;
389
- currentRuntime = runtime;
390
- currentInstance = instance;
501
+ hookRenderState.currentRuntime = runtime;
502
+ hookRenderState.currentInstance = instance;
391
503
 
392
504
  try {
393
- return render();
505
+ return withContextReadObserver((context, value) => {
506
+ (instance.contextDependencies ??= new Map()).set(context, value);
507
+ }, render);
394
508
  } finally {
395
- currentRuntime = previousRuntime;
396
- currentInstance = previousInstance;
509
+ hookRenderState.currentRuntime = previousRuntime;
510
+ hookRenderState.currentInstance = previousInstance;
511
+ }
512
+ }
513
+
514
+ export function hasChangedContextDependency(
515
+ runtime: RootRuntime,
516
+ keys: readonly string[],
517
+ ): boolean {
518
+ for (const key of keys) {
519
+ const dependencies = runtime.instances.get(key)?.contextDependencies;
520
+
521
+ if (dependencies === undefined) {
522
+ continue;
523
+ }
524
+
525
+ for (const [context, value] of dependencies) {
526
+ if (!Object.is(readContextValue(context), value)) {
527
+ return true;
528
+ }
529
+ }
397
530
  }
531
+
532
+ return false;
533
+ }
534
+
535
+ export function hasContextDependency(
536
+ runtime: RootRuntime,
537
+ keys: readonly string[],
538
+ ): boolean {
539
+ return keys.some((key) => runtime.instances.get(key)?.contextDependencies !== undefined);
398
540
  }
399
541
 
400
542
  export function getDevToolsHookState(
@@ -426,10 +568,54 @@ export function renderWithStrictMode<T>(
426
568
  }
427
569
  }
428
570
 
571
+ export function renderWithStrictModeMemoCapture<T>(
572
+ runtime: RootRuntime,
573
+ render: () => T,
574
+ ): { result: T; memoValues: readonly unknown[]; memoValuesByHook: ReadonlyMap<string, unknown> } {
575
+ const previousCapture = runtime.strictMemoCapture;
576
+ const previousCaptureByHook = runtime.strictMemoCaptureByHook;
577
+ runtime.strictMemoCapture = [];
578
+ runtime.strictMemoCaptureByHook = new Map();
579
+
580
+ try {
581
+ const result = renderWithStrictMode(runtime, render);
582
+ return {
583
+ result,
584
+ memoValues: runtime.strictMemoCapture,
585
+ memoValuesByHook: runtime.strictMemoCaptureByHook,
586
+ };
587
+ } finally {
588
+ runtime.strictMemoCapture = previousCapture;
589
+ runtime.strictMemoCaptureByHook = previousCaptureByHook;
590
+ }
591
+ }
592
+
593
+ export function renderStrictModeReplay<T>(
594
+ runtime: RootRuntime,
595
+ memoValues: readonly unknown[],
596
+ memoValuesByHook: ReadonlyMap<string, unknown>,
597
+ render: () => T,
598
+ ): T {
599
+ const previousReplay = runtime.strictMemoReplay;
600
+ const previousReplayByHook = runtime.strictMemoReplayByHook;
601
+ runtime.strictReplayDepth += 1;
602
+ runtime.strictMemoReplay = { values: memoValues, index: 0 };
603
+ runtime.strictMemoReplayByHook = memoValuesByHook;
604
+
605
+ try {
606
+ return render();
607
+ } finally {
608
+ runtime.strictMemoReplay = previousReplay;
609
+ runtime.strictMemoReplayByHook = previousReplayByHook;
610
+ runtime.strictReplayDepth -= 1;
611
+ }
612
+ }
613
+
429
614
  export function takeRuntimeSnapshot(runtime: RootRuntime): RuntimeSnapshot {
430
615
  return {
431
616
  instanceKeys: new Set(runtime.instances.keys()),
432
617
  portalContainers: new Set(runtime.portalContainers),
618
+ portalNodes: clonePortalNodes(runtime.portalNodes),
433
619
  pendingInsertionEffectsLength: runtime.pendingInsertionEffects.length,
434
620
  pendingLayoutEffectsLength: runtime.pendingLayoutEffects.length,
435
621
  pendingEffectsLength: runtime.pendingEffects.length,
@@ -439,6 +625,11 @@ export function takeRuntimeSnapshot(runtime: RootRuntime): RuntimeSnapshot {
439
625
  identifierPrefix: runtime.identifierPrefix,
440
626
  idMode: runtime.idMode,
441
627
  strictModeDepth: runtime.strictModeDepth,
628
+ strictReplayDepth: runtime.strictReplayDepth,
629
+ strictMemoCapture: runtime.strictMemoCapture,
630
+ strictMemoCaptureByHook: runtime.strictMemoCaptureByHook,
631
+ strictMemoReplay: runtime.strictMemoReplay,
632
+ strictMemoReplayByHook: runtime.strictMemoReplayByHook,
442
633
  profilerFlushDepth: runtime.profilerFlushDepth,
443
634
  };
444
635
  }
@@ -456,6 +647,11 @@ export function restoreRuntimeSnapshot(
456
647
  runtime.identifierPrefix = snapshot.identifierPrefix;
457
648
  runtime.idMode = snapshot.idMode;
458
649
  runtime.strictModeDepth = snapshot.strictModeDepth;
650
+ runtime.strictReplayDepth = snapshot.strictReplayDepth;
651
+ runtime.strictMemoCapture = snapshot.strictMemoCapture;
652
+ runtime.strictMemoCaptureByHook = snapshot.strictMemoCaptureByHook;
653
+ runtime.strictMemoReplay = snapshot.strictMemoReplay;
654
+ runtime.strictMemoReplayByHook = snapshot.strictMemoReplayByHook;
459
655
  runtime.profilerFlushDepth = snapshot.profilerFlushDepth;
460
656
 
461
657
  for (const key of runtime.instances.keys()) {
@@ -464,16 +660,48 @@ export function restoreRuntimeSnapshot(
464
660
  }
465
661
  }
466
662
 
467
- for (const container of runtime.portalContainers) {
468
- if (!snapshot.portalContainers.has(container)) {
469
- container.replaceChildren();
663
+ clearRuntimePortalNodesExcept(runtime, snapshot.portalNodes);
664
+ runtime.portalContainers.clear();
665
+ for (const container of snapshot.portalContainers) {
666
+ runtime.portalContainers.add(container);
667
+ }
668
+ runtime.portalNodes = clonePortalNodes(snapshot.portalNodes);
669
+ }
670
+
671
+ export function clearRuntimePortalNodes(runtime: RootRuntime): void {
672
+ for (const [container, nodes] of runtime.portalNodes) {
673
+ for (const node of nodes) {
674
+ removeChildIfPresent(container, node);
470
675
  }
471
676
  }
472
677
 
678
+ runtime.portalNodes.clear();
473
679
  runtime.portalContainers.clear();
474
- for (const container of snapshot.portalContainers) {
475
- runtime.portalContainers.add(container);
680
+ }
681
+
682
+ function clearRuntimePortalNodesExcept(
683
+ runtime: RootRuntime,
684
+ preserved: Map<Element, Set<Node>>,
685
+ ): void {
686
+ for (const [container, nodes] of runtime.portalNodes) {
687
+ const preservedNodes = preserved.get(container);
688
+
689
+ for (const node of nodes) {
690
+ if (preservedNodes?.has(node) !== true) {
691
+ removeChildIfPresent(container, node);
692
+ }
693
+ }
694
+ }
695
+ }
696
+
697
+ function clonePortalNodes(source: Map<Element, Set<Node>>): Map<Element, Set<Node>> {
698
+ const clone = new Map<Element, Set<Node>>();
699
+
700
+ for (const [container, nodes] of source) {
701
+ clone.set(container, new Set(nodes));
476
702
  }
703
+
704
+ return clone;
477
705
  }
478
706
 
479
707
  export function useState<T>(
@@ -499,6 +727,7 @@ export function useState<T>(
499
727
  }
500
728
 
501
729
  const setState = (value: T | ((previous: T) => T)): void => {
730
+ const previousValue = slot.value;
502
731
  const nextValue =
503
732
  typeof value === "function"
504
733
  ? (value as (previous: T) => T)(slot.value as T)
@@ -508,7 +737,17 @@ export function useState<T>(
508
737
  return;
509
738
  }
510
739
 
740
+ if (hookRenderState.hostCommitDepth > 0 && !Object.hasOwn(slot, "hostCommitValue")) {
741
+ slot.hostCommitValue = previousValue;
742
+ }
743
+
511
744
  slot.value = nextValue;
745
+ if (hookRenderState.hostCommitDepth > 0) {
746
+ updateHostCommitDirtyState(instance);
747
+ hookRenderState.queuedHostCommitRerenders.add(runtime);
748
+ return;
749
+ }
750
+
512
751
  scheduleInstanceUpdate(runtime, instance);
513
752
  };
514
753
 
@@ -616,6 +855,7 @@ export function useImperativeHandle<T>(
616
855
  }
617
856
 
618
857
  export function useMemo<T>(factory: () => T, deps?: readonly unknown[]): T {
858
+ const runtime = requireRuntime();
619
859
  const instance = requireInstance();
620
860
  const index = instance.hookIndex;
621
861
  instance.hookIndex += 1;
@@ -626,25 +866,82 @@ export function useMemo<T>(factory: () => T, deps?: readonly unknown[]): T {
626
866
  throw new Error("Hook order changed between renders.");
627
867
  }
628
868
 
629
- if (
869
+ let value: unknown;
870
+ const shouldRecompute =
630
871
  slot === undefined ||
631
872
  deps === undefined ||
632
873
  slot.deps === undefined ||
633
- !areHookInputsEqual(deps, slot.deps)
634
- ) {
635
- const value = factory();
874
+ !areHookInputsEqual(deps, slot.deps);
875
+
876
+ if (runtime.strictReplayDepth > 0) {
877
+ const replayValue = factory();
878
+ if (slot === undefined) {
879
+ slot =
880
+ deps === undefined
881
+ ? { kind: "memo", value: replayValue }
882
+ : { kind: "memo", value: replayValue, deps };
883
+ instance.hooks[index] = slot;
884
+ }
885
+ const hookKey = getStrictMemoHookKey(instance, index);
886
+ const replayByHook = runtime.strictMemoReplayByHook;
887
+ const replay = runtime.strictMemoReplay;
888
+ value = replayByHook?.has(hookKey) === true
889
+ ? replayByHook.get(hookKey)
890
+ : replay === undefined || replay.index >= replay.values.length
891
+ ? slot.value
892
+ : replay.values[replay.index++];
893
+ } else if (shouldRecompute) {
894
+ value = factory();
636
895
  slot =
637
896
  deps === undefined
638
897
  ? { kind: "memo", value }
639
898
  : { kind: "memo", value, deps };
640
899
  instance.hooks[index] = slot;
900
+ } else {
901
+ value = slot!.value;
641
902
  }
642
903
 
643
- recordDevToolsHook("useMemo", slot.deps === undefined
644
- ? { kind: "memo", value: slot.value }
645
- : { kind: "memo", value: slot.value, deps: slot.deps });
904
+ if (runtime.strictModeDepth > 0 && runtime.strictReplayDepth === 0) {
905
+ runtime.strictMemoCapture?.push(value);
906
+ runtime.strictMemoCaptureByHook?.set(getStrictMemoHookKey(instance, index), value);
907
+ }
646
908
 
647
- return slot.value as T;
909
+ const memoSlot = slot;
910
+ if (memoSlot === undefined) {
911
+ throw new Error("Hook order changed between renders.");
912
+ }
913
+
914
+ recordDevToolsHook("useMemo", memoSlot.deps === undefined
915
+ ? { kind: "memo", value }
916
+ : { kind: "memo", value, deps: memoSlot.deps });
917
+
918
+ return value as T;
919
+ }
920
+
921
+ function getStrictMemoHookKey(
922
+ instance: ComponentInstance,
923
+ index: number,
924
+ ): string {
925
+ return `${instance.path}:${getStrictMemoOwnerId(instance.owner)}:${index}`;
926
+ }
927
+
928
+ function getStrictMemoOwnerId(owner: unknown): number {
929
+ if ((typeof owner === "object" && owner !== null) || typeof owner === "function") {
930
+ const objectOwner = owner as object;
931
+ let ownerId = strictMemoObjectOwnerIds.get(objectOwner);
932
+ if (ownerId === undefined) {
933
+ ownerId = strictMemoOwnerId++;
934
+ strictMemoObjectOwnerIds.set(objectOwner, ownerId);
935
+ }
936
+ return ownerId;
937
+ }
938
+
939
+ let ownerId = strictMemoPrimitiveOwnerIds.get(owner);
940
+ if (ownerId === undefined) {
941
+ ownerId = strictMemoOwnerId++;
942
+ strictMemoPrimitiveOwnerIds.set(owner, ownerId);
943
+ }
944
+ return ownerId;
648
945
  }
649
946
 
650
947
  function assignRef<T>(ref: unknown, value: T | null): void {
@@ -659,7 +956,7 @@ function assignRef<T>(ref: unknown, value: T | null): void {
659
956
  }
660
957
 
661
958
  function recordDevToolsHook(type: string, value: DevToolsHookValue): void {
662
- const instance = currentInstance;
959
+ const instance = hookRenderState.currentInstance;
663
960
 
664
961
  if (instance === undefined || instance.devToolsHookSuppressionDepth > 0) {
665
962
  return;
@@ -797,16 +1094,32 @@ export function useSyncExternalStore<T>(
797
1094
 
798
1095
  runWithoutDevToolsHookTracking(() => useEffect(() => {
799
1096
  const checkForUpdates = (): void => {
1097
+ if (instance.disposed === true) {
1098
+ return;
1099
+ }
1100
+
800
1101
  const nextSnapshot = getSnapshot();
801
1102
 
802
1103
  if (!Object.is(slot.value, nextSnapshot)) {
803
1104
  slot.value = nextSnapshot;
1105
+ instance.dirty = true;
1106
+ runtime.externalStoreUpdate = true;
1107
+ if (runtime.profilerFlushDepth > 0) {
1108
+ hookRenderState.queuedEffectFlushRerenders.add(runtime);
1109
+ return;
1110
+ }
804
1111
  runtime.rerender("sync");
805
1112
  }
806
1113
  };
807
1114
 
808
- checkForUpdates();
809
- return subscribe(checkForUpdates);
1115
+ if (slot.hasMounted !== true) {
1116
+ checkForUpdates();
1117
+ }
1118
+ const unsubscribe = subscribe(checkForUpdates);
1119
+ slot.hasMounted = true;
1120
+ return () => {
1121
+ unsubscribe();
1122
+ };
810
1123
  }, [subscribe, getSnapshot]));
811
1124
 
812
1125
  recordDevToolsHook("useSyncExternalStore", {
@@ -981,418 +1294,6 @@ export function hasStableExternalStores(
981
1294
  );
982
1295
  }
983
1296
 
984
- export function renderToString<TProps>(
985
- component:
986
- | ((props: TProps) => ReactCompatNode)
987
- | (new (props: TProps) => { render(): ReactCompatNode }),
988
- props?: TProps,
989
- options: RootRuntimeOptions = {},
990
- ): string {
991
- const runtime = createRootRuntime(() => undefined, {
992
- ...options,
993
- idMode: "server",
994
- });
995
-
996
- return runWithCacheScope(createCacheScope(), () => {
997
- try {
998
- const rendered = renderWithRootRuntime(runtime, "0", () => {
999
- if (isClassComponentType(component)) {
1000
- const instance = new component(props as Record<string, unknown>);
1001
- return instance.render();
1002
- }
1003
-
1004
- return (component as (props: TProps) => ReactCompatNode)(props as TProps);
1005
- });
1006
- return typeof rendered === "string"
1007
- ? rendered
1008
- : renderNodeToString(rendered, runtime, "0");
1009
- } finally {
1010
- runtime.dispose();
1011
- }
1012
- });
1013
- }
1014
-
1015
- function renderNodeToString(
1016
- node: ReactCompatNode,
1017
- runtime: RootRuntime,
1018
- path: string,
1019
- ): string {
1020
- if (node === null || node === undefined || typeof node === "boolean") {
1021
- return "";
1022
- }
1023
-
1024
- if (typeof node === "string" || typeof node === "number") {
1025
- return escapeHtml(node);
1026
- }
1027
-
1028
- if (Array.isArray(node)) {
1029
- return node.map((child, index) => renderNodeToString(child, runtime, `${path}.${index}`)).join("");
1030
- }
1031
-
1032
- if (!isReactCompatElement(node)) {
1033
- return "";
1034
- }
1035
-
1036
- return renderElementToString(node, runtime, path);
1037
- }
1038
-
1039
- function renderElementToString(
1040
- element: ReactCompatElement,
1041
- runtime: RootRuntime,
1042
- path: string,
1043
- ): string {
1044
- if (typeof element.type === "string") {
1045
- if (element.type === "textarea") {
1046
- return renderTextareaToString(element, runtime, path);
1047
- }
1048
-
1049
- if (element.type === "select") {
1050
- return renderSelectToString(element, runtime, path);
1051
- }
1052
-
1053
- const attributes = element.type === "input"
1054
- ? renderInputAttributesToString(element.props)
1055
- : Object.entries(element.props)
1056
- .map(([name, value]) => renderHtmlAttribute(name, value))
1057
- .filter((attribute) => attribute !== "")
1058
- .join("");
1059
- if (voidHtmlElements.has(element.type)) {
1060
- return `<${element.type}${attributes}/>`;
1061
- }
1062
-
1063
- return `<${element.type}${attributes}>${renderNodeToString(element.props.children, runtime, `${path}.children`)}</${element.type}>`;
1064
- }
1065
-
1066
- if (element.type === Fragment) {
1067
- return renderNodeToString(element.props.children, runtime, `${path}.fragment`);
1068
- }
1069
-
1070
- if (element.type === Activity) {
1071
- if ((element.props as { mode?: unknown }).mode === "hidden") {
1072
- return "";
1073
- }
1074
-
1075
- return `<!--&-->${renderNodeToString(element.props.children, runtime, `${path}.activity`)}<!--/&-->`;
1076
- }
1077
-
1078
- if (element.type === Profiler) {
1079
- return renderNodeToString(element.props.children, runtime, `${path}.profiler`);
1080
- }
1081
-
1082
- if (isReactCompatProvider(element.type)) {
1083
- return renderWithContextProvider(
1084
- element.type,
1085
- (element.props as { value?: unknown }).value,
1086
- () => renderNodeToString(element.props.children, runtime, `${path}.provider`),
1087
- );
1088
- }
1089
-
1090
- if (isReactCompatConsumer(element.type)) {
1091
- const children = element.props.children;
1092
-
1093
- if (typeof children === "function") {
1094
- return renderNodeToString(
1095
- (children as (value: unknown) => ReactCompatNode)(useContext(element.type.context)),
1096
- runtime,
1097
- `${path}.consumer`,
1098
- );
1099
- }
1100
-
1101
- return "";
1102
- }
1103
-
1104
- if (isForwardRefType(element.type)) {
1105
- const forwardRefType = element.type;
1106
- return renderNodeToString(
1107
- renderWithRootRuntime(runtime, `${path}.forwardRef`, () =>
1108
- forwardRefType.render(element.props, element.ref),
1109
- ),
1110
- runtime,
1111
- `${path}.forwardRef`,
1112
- );
1113
- }
1114
-
1115
- if (isMemoType(element.type)) {
1116
- return renderNodeToString(
1117
- {
1118
- ...element,
1119
- type: element.type.type,
1120
- },
1121
- runtime,
1122
- `${path}.memo`,
1123
- );
1124
- }
1125
-
1126
- if (isClassComponentType(element.type)) {
1127
- const instance = new element.type(element.props);
1128
- return renderNodeToString(
1129
- renderWithRootRuntime(runtime, path, () => instance.render()),
1130
- runtime,
1131
- path,
1132
- );
1133
- }
1134
-
1135
- if (typeof element.type === "function") {
1136
- const component = element.type as (props: typeof element.props) => ReactCompatNode;
1137
- return renderNodeToString(
1138
- renderWithRootRuntime(runtime, path, () => component(element.props)),
1139
- runtime,
1140
- path,
1141
- );
1142
- }
1143
-
1144
- return "";
1145
- }
1146
-
1147
- function isClassComponentType(
1148
- value: unknown,
1149
- ): value is new (props: Record<string, unknown>) => { render(): ReactCompatNode } {
1150
- return (
1151
- typeof value === "function" &&
1152
- typeof (value as { prototype?: { render?: unknown } }).prototype?.render === "function"
1153
- );
1154
- }
1155
-
1156
- function renderTextareaToString(
1157
- element: ReactCompatElement,
1158
- runtime: RootRuntime,
1159
- path: string,
1160
- ): string {
1161
- const value =
1162
- (element.props as { value?: unknown; defaultValue?: unknown }).value ??
1163
- (element.props as { value?: unknown; defaultValue?: unknown }).defaultValue ??
1164
- element.props.children;
1165
- const attributes = Object.entries(element.props)
1166
- .filter(([name]) => name !== "value" && name !== "defaultValue")
1167
- .map(([name, child]) => renderHtmlAttribute(name, child))
1168
- .filter((attribute) => attribute !== "")
1169
- .join("");
1170
-
1171
- return `<textarea${attributes}>${renderNodeToString(value as ReactCompatNode, runtime, `${path}.textarea`)}</textarea>`;
1172
- }
1173
-
1174
- function renderSelectToString(
1175
- element: ReactCompatElement,
1176
- runtime: RootRuntime,
1177
- path: string,
1178
- ): string {
1179
- const selectedValue =
1180
- (element.props as { value?: unknown; defaultValue?: unknown }).value ??
1181
- (element.props as { value?: unknown; defaultValue?: unknown }).defaultValue;
1182
- const attributes = Object.entries(element.props)
1183
- .filter(([name]) => name !== "value" && name !== "defaultValue")
1184
- .map(([name, child]) => renderHtmlAttribute(name, child))
1185
- .filter((attribute) => attribute !== "")
1186
- .join("");
1187
-
1188
- return `<select${attributes}>${renderSelectChildrenToString(
1189
- element.props.children,
1190
- selectedValue,
1191
- runtime,
1192
- `${path}.select`,
1193
- )}</select>`;
1194
- }
1195
-
1196
- function renderSelectChildrenToString(
1197
- children: ReactCompatNode,
1198
- selectedValue: unknown,
1199
- runtime: RootRuntime,
1200
- path: string,
1201
- ): string {
1202
- const childArray = Array.isArray(children) ? children : [children];
1203
-
1204
- return childArray.map((child, index) => {
1205
- if (!isReactCompatElement(child) || child.type !== "option") {
1206
- return renderNodeToString(child, runtime, `${path}.${index}`);
1207
- }
1208
-
1209
- const optionValue =
1210
- (child.props as { value?: unknown }).value ?? child.props.children;
1211
- const selected =
1212
- selectedValue !== undefined && String(optionValue) === String(selectedValue);
1213
- const props = selected
1214
- ? { ...child.props, selected: true }
1215
- : child.props;
1216
-
1217
- return renderElementToString({ ...child, props }, runtime, `${path}.${index}`);
1218
- }).join("");
1219
- }
1220
-
1221
- function renderInputAttributesToString(props: Record<string, unknown>): string {
1222
- const hasValue = props.value !== undefined;
1223
- const hasChecked = props.checked !== undefined;
1224
-
1225
- return Object.entries(props)
1226
- .filter(([name]) =>
1227
- !((name === "defaultValue" && hasValue) || (name === "defaultChecked" && hasChecked))
1228
- )
1229
- .sort(([leftName], [rightName]) =>
1230
- Number(isInputValueAttribute(leftName)) - Number(isInputValueAttribute(rightName))
1231
- )
1232
- .map(([name, value]) => renderHtmlAttribute(toInputHtmlAttributeName(name), value))
1233
- .filter((attribute) => attribute !== "")
1234
- .join("");
1235
- }
1236
-
1237
- function renderHtmlAttribute(name: string, value: unknown): string {
1238
- if (
1239
- name === "children" ||
1240
- name === "key" ||
1241
- name === "ref" ||
1242
- /^on[A-Z]/.test(name) ||
1243
- value === null ||
1244
- value === undefined ||
1245
- value === false ||
1246
- typeof value === "function"
1247
- ) {
1248
- return "";
1249
- }
1250
-
1251
- if (name === "style") {
1252
- const style = renderStyleAttribute(value);
1253
- return style === "" ? "" : ` style="${escapeHtml(style)}"`;
1254
- }
1255
-
1256
- const attributeName = toHtmlAttributeName(name);
1257
- if (isDangerousHtmlAttribute(attributeName)) {
1258
- return isDangerousHtmlOptIn(value)
1259
- ? ` ${attributeName}="${escapeHtml(value.__html)}"`
1260
- : "";
1261
- }
1262
-
1263
- if (typeof value === "object") {
1264
- return "";
1265
- }
1266
-
1267
- if (value === true) {
1268
- return ` ${attributeName}=""`;
1269
- }
1270
-
1271
- return ` ${attributeName}="${escapeHtml(value)}"`;
1272
- }
1273
-
1274
- function isInputValueAttribute(name: string): boolean {
1275
- return name === "value" || name === "defaultValue";
1276
- }
1277
-
1278
- function toInputHtmlAttributeName(name: string): string {
1279
- if (name === "defaultValue") {
1280
- return "value";
1281
- }
1282
-
1283
- if (name === "defaultChecked") {
1284
- return "checked";
1285
- }
1286
-
1287
- return name;
1288
- }
1289
-
1290
- function toHtmlAttributeName(name: string): string {
1291
- return HTML_ATTRIBUTE_ALIASES[name] ?? name;
1292
- }
1293
-
1294
- const HTML_ATTRIBUTE_ALIASES: Record<string, string> = {
1295
- acceptCharset: "accept-charset",
1296
- autoFocus: "autofocus",
1297
- autoPlay: "autoplay",
1298
- charSet: "charset",
1299
- className: "class",
1300
- colSpan: "colspan",
1301
- contentEditable: "contenteditable",
1302
- crossOrigin: "crossorigin",
1303
- encType: "enctype",
1304
- formAction: "formaction",
1305
- frameBorder: "frameborder",
1306
- htmlFor: "for",
1307
- httpEquiv: "http-equiv",
1308
- maxLength: "maxlength",
1309
- minLength: "minlength",
1310
- noValidate: "novalidate",
1311
- playsInline: "playsinline",
1312
- readOnly: "readOnly",
1313
- rowSpan: "rowspan",
1314
- spellCheck: "spellcheck",
1315
- srcDoc: "srcdoc",
1316
- srcSet: "srcset",
1317
- tabIndex: "tabindex",
1318
- useMap: "usemap",
1319
- };
1320
-
1321
- function renderStyleAttribute(value: unknown): string {
1322
- if (typeof value !== "object" || value === null) {
1323
- return "";
1324
- }
1325
-
1326
- return Object.entries(value)
1327
- .filter(([, propertyValue]) =>
1328
- propertyValue !== null &&
1329
- propertyValue !== undefined &&
1330
- typeof propertyValue !== "boolean" &&
1331
- propertyValue !== "",
1332
- )
1333
- .map(([name, propertyValue]) =>
1334
- `${toKebabCase(name)}:${renderCssValue(name, propertyValue)}`,
1335
- )
1336
- .join(";");
1337
- }
1338
-
1339
- function renderCssValue(name: string, value: unknown): string {
1340
- if (typeof value !== "number" || value === 0 || isUnitlessCssProperty(name)) {
1341
- return String(value);
1342
- }
1343
-
1344
- return `${value}px`;
1345
- }
1346
-
1347
- function toKebabCase(value: string): string {
1348
- return value.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`);
1349
- }
1350
-
1351
- function isUnitlessCssProperty(name: string): boolean {
1352
- return (
1353
- name === "flex" ||
1354
- name === "fontWeight" ||
1355
- name === "lineHeight" ||
1356
- name === "opacity" ||
1357
- name === "order" ||
1358
- name === "zIndex" ||
1359
- name === "zoom"
1360
- );
1361
- }
1362
-
1363
- const voidHtmlElements = new Set([
1364
- "area",
1365
- "base",
1366
- "br",
1367
- "col",
1368
- "embed",
1369
- "hr",
1370
- "img",
1371
- "input",
1372
- "link",
1373
- "meta",
1374
- "param",
1375
- "source",
1376
- "track",
1377
- "wbr",
1378
- ]);
1379
-
1380
- function isForwardRefType(value: unknown): value is ForwardRefType {
1381
- return (
1382
- typeof value === "object" &&
1383
- value !== null &&
1384
- (value as { $$typeof?: unknown }).$$typeof === FORWARD_REF_TYPE
1385
- );
1386
- }
1387
-
1388
- function isMemoType(value: unknown): value is MemoType {
1389
- return (
1390
- typeof value === "object" &&
1391
- value !== null &&
1392
- (value as { $$typeof?: unknown }).$$typeof === MEMO_TYPE
1393
- );
1394
- }
1395
-
1396
1297
  function readThenable<T>(thenable: PromiseLike<T>): T {
1397
1298
  const record = thenable as PromiseLike<T> & {
1398
1299
  status?: "pending" | "fulfilled" | "rejected";
@@ -1482,6 +1383,15 @@ export function flushSyncUpdates<T>(callback: () => T): T {
1482
1383
  }
1483
1384
  }
1484
1385
 
1386
+ export function runWithHostCommit<T>(callback: () => T): T {
1387
+ hookRenderState.hostCommitDepth += 1;
1388
+ try {
1389
+ return callback();
1390
+ } finally {
1391
+ hookRenderState.hostCommitDepth -= 1;
1392
+ }
1393
+ }
1394
+
1485
1395
  export function useTransition(): [boolean, StartTransition] {
1486
1396
  const [pending, setPending] = runWithoutDevToolsHookTracking(() => useState(false));
1487
1397
  const startTransitionWithPending: StartTransition = (scope) => {
@@ -1640,6 +1550,7 @@ function useEffectImpl(
1640
1550
 
1641
1551
  const shouldRun =
1642
1552
  slot === undefined ||
1553
+ slot.mounted !== true ||
1643
1554
  deps === undefined ||
1644
1555
  slot.deps === undefined ||
1645
1556
  !areHookInputsEqual(deps, slot.deps);
@@ -1663,7 +1574,8 @@ function useEffectImpl(
1663
1574
  }
1664
1575
 
1665
1576
  slot.strictReplay =
1666
- runtime.strictModeDepth > 0 && effectKind !== "insertion";
1577
+ (runtime.strictModeDepth > 0 || runtime.strictReplayDepth > 0) &&
1578
+ effectKind !== "insertion";
1667
1579
 
1668
1580
  if (shouldRun) {
1669
1581
  const queue =
@@ -1680,7 +1592,7 @@ function recordExternalStoreCheck<T>(
1680
1592
  getSnapshot: () => T,
1681
1593
  value: T,
1682
1594
  ): void {
1683
- currentRuntime?.externalStoreChecks.push({ getSnapshot, value });
1595
+ hookRenderState.currentRuntime?.externalStoreChecks.push({ getSnapshot, value });
1684
1596
  }
1685
1597
 
1686
1598
  function flushPendingEffects(queue: PendingEffect[]): PendingEffect[] {
@@ -1701,6 +1613,7 @@ function flushPendingEffects(queue: PendingEffect[]): PendingEffect[] {
1701
1613
  } else {
1702
1614
  delete slot.cleanup;
1703
1615
  }
1616
+ slot.mounted = true;
1704
1617
 
1705
1618
  if (shouldReplay) {
1706
1619
  strictReplay.push({ slot });
@@ -1795,9 +1708,21 @@ function scheduleInstanceUpdate(
1795
1708
  runtime: RootRuntime,
1796
1709
  instance: ComponentInstance,
1797
1710
  ): void {
1711
+ if (instance.disposed === true) {
1712
+ return;
1713
+ }
1714
+
1798
1715
  instance.dirty = true;
1799
1716
  if (transitionDepth === 0) {
1800
1717
  syncVersion += 1;
1718
+ if (hookRenderState.hostCommitDepth > 0) {
1719
+ hookRenderState.queuedHostCommitRerenders.add(runtime);
1720
+ return;
1721
+ }
1722
+ if (runtime.effectFlushPhase !== undefined) {
1723
+ hookRenderState.queuedEffectFlushRerenders.add(runtime);
1724
+ return;
1725
+ }
1801
1726
  if (eventBatchDepth > 0) {
1802
1727
  queueEventRerender(runtime);
1803
1728
  return;
@@ -1811,6 +1736,82 @@ function scheduleInstanceUpdate(
1811
1736
  }
1812
1737
  }
1813
1738
 
1739
+ function flushHostCommitRerenders(): void {
1740
+ if (
1741
+ hookRenderState.hostCommitDepth > 0 ||
1742
+ hookRenderState.queuedHostCommitRerenders.size === 0
1743
+ ) {
1744
+ return;
1745
+ }
1746
+
1747
+ const runtimes = [...hookRenderState.queuedHostCommitRerenders];
1748
+ hookRenderState.queuedHostCommitRerenders.clear();
1749
+ for (const runtime of runtimes) {
1750
+ const hasDirtyInstance = Array.from(runtime.instances.values()).some(
1751
+ (instance) => instance.dirty,
1752
+ );
1753
+ clearHostCommitStateBaselines(runtime);
1754
+
1755
+ if (hasDirtyInstance) {
1756
+ runtime.rerender("sync");
1757
+ }
1758
+ }
1759
+ }
1760
+
1761
+ function flushEffectFlushRerenders(): void {
1762
+ if (
1763
+ effectFlushRerenderDepth > 0 ||
1764
+ hookRenderState.queuedEffectFlushRerenders.size === 0
1765
+ ) {
1766
+ return;
1767
+ }
1768
+
1769
+ effectFlushRerenderDepth += 1;
1770
+ try {
1771
+ for (
1772
+ let attempt = 0;
1773
+ attempt < 3 && hookRenderState.queuedEffectFlushRerenders.size > 0;
1774
+ attempt += 1
1775
+ ) {
1776
+ const runtimes = [...hookRenderState.queuedEffectFlushRerenders];
1777
+ hookRenderState.queuedEffectFlushRerenders.clear();
1778
+ for (const runtime of runtimes) {
1779
+ const hasDirtyInstance = Array.from(runtime.instances.values()).some(
1780
+ (instance) => instance.dirty,
1781
+ );
1782
+
1783
+ if (hasDirtyInstance) {
1784
+ runtime.rerender("sync");
1785
+ }
1786
+ }
1787
+ }
1788
+ hookRenderState.queuedEffectFlushRerenders.clear();
1789
+ } finally {
1790
+ effectFlushRerenderDepth -= 1;
1791
+ }
1792
+ }
1793
+
1794
+ function updateHostCommitDirtyState(
1795
+ instance: ComponentInstance,
1796
+ ): void {
1797
+ instance.dirty = instance.hooks.some(
1798
+ (slot) =>
1799
+ slot.kind === "state" &&
1800
+ Object.hasOwn(slot, "hostCommitValue") &&
1801
+ !Object.is(slot.hostCommitValue, slot.value),
1802
+ );
1803
+ }
1804
+
1805
+ function clearHostCommitStateBaselines(runtime: RootRuntime): void {
1806
+ for (const instance of runtime.instances.values()) {
1807
+ for (const slot of instance.hooks) {
1808
+ if (slot.kind === "state") {
1809
+ delete slot.hostCommitValue;
1810
+ }
1811
+ }
1812
+ }
1813
+ }
1814
+
1814
1815
  function getCacheLeaf(
1815
1816
  scope: CacheScope,
1816
1817
  callback: (...args: never[]) => unknown,
@@ -1861,7 +1862,7 @@ function createCacheTrieNode(): CacheTrieNode {
1861
1862
  }
1862
1863
 
1863
1864
  function getCurrentCacheScope(): CacheScope | undefined {
1864
- return currentCacheScope ?? getGlobalCacheScope();
1865
+ return hookRenderState.currentCacheScope ?? getGlobalCacheScope();
1865
1866
  }
1866
1867
 
1867
1868
  function getGlobalCacheScope(): CacheScope | undefined {
@@ -1901,9 +1902,11 @@ function cleanupInactiveInstances(runtime: RootRuntime): void {
1901
1902
  }
1902
1903
 
1903
1904
  function cleanupInstance(instance: ComponentInstance): void {
1905
+ instance.disposed = true;
1904
1906
  for (const slot of instance.hooks) {
1905
1907
  if (slot?.kind === "effect") {
1906
1908
  slot.disposed = true;
1909
+ slot.mounted = false;
1907
1910
  slot.cleanup?.();
1908
1911
  delete slot.cleanup;
1909
1912
  }
@@ -1911,19 +1914,19 @@ function cleanupInstance(instance: ComponentInstance): void {
1911
1914
  }
1912
1915
 
1913
1916
  function requireRuntime(): RootRuntime {
1914
- if (currentRuntime === undefined) {
1917
+ if (hookRenderState.currentRuntime === undefined) {
1915
1918
  throw new Error("Hooks can only be called while rendering.");
1916
1919
  }
1917
1920
 
1918
- return currentRuntime;
1921
+ return hookRenderState.currentRuntime;
1919
1922
  }
1920
1923
 
1921
1924
  function requireInstance(): ComponentInstance {
1922
- if (currentInstance === undefined) {
1925
+ if (hookRenderState.currentInstance === undefined) {
1923
1926
  throw new Error("Hooks can only be called while rendering.");
1924
1927
  }
1925
1928
 
1926
- return currentInstance;
1929
+ return hookRenderState.currentInstance;
1927
1930
  }
1928
1931
 
1929
1932
  function areHookInputsEqual(