@reckona/mreact-compat 0.0.171 → 0.0.173

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/dist/devtools.js +2 -0
  2. package/dist/devtools.js.map +1 -1
  3. package/dist/dom-host-rules.d.ts.map +1 -1
  4. package/dist/dom-host-rules.js +4 -1
  5. package/dist/dom-host-rules.js.map +1 -1
  6. package/dist/dom-props.js +2 -1
  7. package/dist/dom-props.js.map +1 -1
  8. package/dist/element.d.ts +13 -1
  9. package/dist/element.d.ts.map +1 -1
  10. package/dist/element.js +30 -2
  11. package/dist/element.js.map +1 -1
  12. package/dist/fiber-commit.js +2 -1
  13. package/dist/fiber-commit.js.map +1 -1
  14. package/dist/fiber-host.d.ts +1 -1
  15. package/dist/fiber-host.d.ts.map +1 -1
  16. package/dist/fiber-host.js +1 -1
  17. package/dist/fiber-host.js.map +1 -1
  18. package/dist/fiber.d.ts +2 -1
  19. package/dist/fiber.d.ts.map +1 -1
  20. package/dist/fiber.js +2 -0
  21. package/dist/fiber.js.map +1 -1
  22. package/dist/hooks.d.ts +9 -0
  23. package/dist/hooks.d.ts.map +1 -1
  24. package/dist/hooks.js +127 -44
  25. package/dist/hooks.js.map +1 -1
  26. package/dist/host-reconciler.d.ts +2 -0
  27. package/dist/host-reconciler.d.ts.map +1 -1
  28. package/dist/host-reconciler.js +696 -55
  29. package/dist/host-reconciler.js.map +1 -1
  30. package/dist/jsx-dev-runtime.d.ts +3 -1
  31. package/dist/jsx-dev-runtime.d.ts.map +1 -1
  32. package/dist/jsx-dev-runtime.js +3 -1
  33. package/dist/jsx-dev-runtime.js.map +1 -1
  34. package/dist/jsx-runtime.d.ts +4 -2
  35. package/dist/jsx-runtime.d.ts.map +1 -1
  36. package/dist/jsx-runtime.js +9 -1
  37. package/dist/jsx-runtime.js.map +1 -1
  38. package/dist/reactive-prop-cell.d.ts +13 -0
  39. package/dist/reactive-prop-cell.d.ts.map +1 -0
  40. package/dist/reactive-prop-cell.js +36 -0
  41. package/dist/reactive-prop-cell.js.map +1 -0
  42. package/dist/root.d.ts.map +1 -1
  43. package/dist/root.js +8 -5
  44. package/dist/root.js.map +1 -1
  45. package/package.json +3 -3
  46. package/src/devtools.ts +2 -0
  47. package/src/dom-host-rules.ts +4 -1
  48. package/src/dom-props.ts +2 -1
  49. package/src/element.ts +60 -3
  50. package/src/fiber-commit.ts +3 -1
  51. package/src/fiber-host.ts +2 -0
  52. package/src/fiber.ts +4 -0
  53. package/src/hooks.ts +187 -46
  54. package/src/host-reconciler.ts +1031 -70
  55. package/src/jsx-dev-runtime.ts +4 -0
  56. package/src/jsx-runtime.ts +16 -0
  57. package/src/reactive-prop-cell.ts +67 -0
  58. package/src/root.ts +14 -4
package/src/fiber.ts CHANGED
@@ -15,6 +15,7 @@ export type FiberTag =
15
15
  | "memo"
16
16
  | "lazy"
17
17
  | "profiler"
18
+ | "reactive-dom-block"
18
19
  | "strict-mode"
19
20
  | "suspense"
20
21
  | "suspense-list"
@@ -41,6 +42,7 @@ export interface Fiber {
41
42
  childLanes: Lanes;
42
43
  hydrateExisting: boolean;
43
44
  hasRefSubtree: boolean;
45
+ hasDisposableResources: boolean;
44
46
  hostChildListChanged: boolean;
45
47
  }
46
48
 
@@ -96,6 +98,7 @@ export function createFiber(
96
98
  childLanes: NoLanes,
97
99
  hydrateExisting: false,
98
100
  hasRefSubtree: false,
101
+ hasDisposableResources: false,
99
102
  hostChildListChanged: false,
100
103
  };
101
104
  }
@@ -157,6 +160,7 @@ export function createWorkInProgress(
157
160
  workInProgress.childLanes = current.childLanes;
158
161
  workInProgress.hydrateExisting = false;
159
162
  workInProgress.hasRefSubtree = current.hasRefSubtree;
163
+ workInProgress.hasDisposableResources = current.hasDisposableResources;
160
164
  workInProgress.hostChildListChanged = current.hostChildListChanged;
161
165
  return workInProgress;
162
166
  }
package/src/hooks.ts CHANGED
@@ -1,6 +1,9 @@
1
1
  import {
2
2
  flushPendingComputed,
3
3
  flushQueuedComputations,
4
+ notifySubscribers,
5
+ trackSource,
6
+ type Source,
4
7
  } from "@reckona/mreact-reactive-core/internal";
5
8
  import { scheduleCallback } from "./fiber-scheduler.js";
6
9
  import { removeChildIfPresent } from "./dom-children.js";
@@ -11,7 +14,10 @@ import {
11
14
  useContext,
12
15
  withContextReadObserver,
13
16
  } from "./context.js";
14
- import { REACTIVE_TEXT_BINDING_META } from "./element.js";
17
+ import {
18
+ REACTIVE_STATE_BINDING_META,
19
+ REACTIVE_TEXT_BINDING_META,
20
+ } from "./element.js";
15
21
  import { isThenable } from "./thenable.js";
16
22
 
17
23
  export interface RootRuntime {
@@ -127,7 +133,14 @@ export type DevToolsHookValue =
127
133
  | { kind: "effect"; effectKind: "insertion" | "layout" | "normal"; deps?: readonly unknown[] };
128
134
 
129
135
  type HookSlot =
130
- | { kind: "state"; value: unknown; hostCommitValue?: unknown; textBinding?: ReactiveTextBinding }
136
+ | {
137
+ kind: "state";
138
+ value: unknown;
139
+ dispatch?: (value: unknown) => void;
140
+ hostCommitValue?: unknown;
141
+ textBinding?: ReactiveTextBinding;
142
+ stateBinding?: ReactiveStateBinding;
143
+ }
131
144
  | {
132
145
  kind: "action-state";
133
146
  state: unknown;
@@ -143,7 +156,13 @@ type HookSlot =
143
156
  update: (state: unknown, payload: unknown) => unknown;
144
157
  dispatch?: (payload: unknown) => void;
145
158
  }
146
- | { kind: "store"; value: unknown; hasMounted?: boolean; hostCommitValue?: unknown }
159
+ | {
160
+ kind: "store";
161
+ value: unknown;
162
+ getSnapshot?: () => unknown;
163
+ hasMounted?: boolean;
164
+ hostCommitValue?: unknown;
165
+ }
147
166
  | { kind: "ref"; value: { current: unknown } }
148
167
  | { kind: "memo"; value: unknown; deps?: readonly unknown[] }
149
168
  | { kind: "debug"; value: unknown }
@@ -158,9 +177,21 @@ type HookSlot =
158
177
  strictReplay?: boolean;
159
178
  };
160
179
 
180
+ interface PendingInstanceContext {
181
+ runtime: RootRuntime;
182
+ path: string;
183
+ owner: unknown;
184
+ existing: ComponentInstance | undefined;
185
+ }
186
+
161
187
  interface HookRenderState {
162
188
  currentRuntime: RootRuntime | undefined;
163
189
  currentInstance: ComponentInstance | undefined;
190
+ // Deferred instance for the component currently rendering. The instance is
191
+ // only materialized (created, registered, prefix-indexed) when a hook or a
192
+ // context read first needs it, so components with no hooks/context (e.g. a
193
+ // pure host-rendering memo row) pay none of that per-render cost.
194
+ pendingInstance: PendingInstanceContext | undefined;
164
195
  currentCacheScope: CacheScope | undefined;
165
196
  hostCommitDepth: number;
166
197
  queuedHostCommitRerenders: Set<RootRuntime>;
@@ -174,6 +205,7 @@ const hookRenderState =
174
205
  ] ??= {
175
206
  currentRuntime: undefined,
176
207
  currentInstance: undefined,
208
+ pendingInstance: undefined,
177
209
  currentCacheScope: undefined,
178
210
  hostCommitDepth: 0,
179
211
  queuedHostCommitRerenders: new Set<RootRuntime>(),
@@ -204,6 +236,12 @@ export interface ReactiveTextBinding {
204
236
  subscribers: Set<Text>;
205
237
  }
206
238
 
239
+ export interface ReactiveStateBinding {
240
+ get(): unknown;
241
+ source: Source;
242
+ value: unknown;
243
+ }
244
+
207
245
  const reactiveTextBindingsByNode = new WeakMap<Text, ReactiveTextBinding>();
208
246
  const hydratedIdsByRuntime = new WeakMap<RootRuntime, Map<string, string>>();
209
247
 
@@ -378,6 +416,9 @@ export function createRootRuntime(
378
416
  flushPendingEffects(this.pendingImperativeHandleEffects);
379
417
  this.effectFlushPhase = "layout";
380
418
  const strictLayoutEffects = flushPendingEffects(this.pendingLayoutEffects);
419
+ if (flushHostCommitRerenders()) {
420
+ dedupePendingEffects(this.pendingEffects);
421
+ }
381
422
  this.effectFlushPhase = "normal";
382
423
  const strictEffects = flushPendingEffects(this.pendingEffects);
383
424
  this.effectFlushPhase = undefined;
@@ -502,25 +543,56 @@ export function renderWithRootRuntime<T>(
502
543
  ): T {
503
544
  const previousRuntime = hookRenderState.currentRuntime;
504
545
  const previousInstance = hookRenderState.currentInstance;
505
- let instance = runtime.instances.get(path);
546
+ const previousPending = hookRenderState.pendingInstance;
506
547
 
507
- if (instance !== undefined && owner !== undefined && instance.owner !== owner) {
508
- cleanupInstance(instance);
509
- instance = undefined;
548
+ let existing = runtime.instances.get(path);
549
+ if (existing !== undefined && owner !== undefined && existing.owner !== owner) {
550
+ cleanupInstance(existing);
551
+ runtime.instances.delete(path);
552
+ removeInstanceKeyFromIndex(runtime, path);
553
+ existing = undefined;
510
554
  }
511
555
 
512
- instance ??= {
513
- owner,
514
- path,
515
- hooks: [],
516
- hookIndex: 0,
517
- dirty: false,
518
- devToolsHookSuppressionDepth: 0,
519
- };
520
- instance.owner = owner;
521
- instance.path = path;
522
- runtime.instances.set(path, instance);
523
- indexInstanceKey(runtime, path);
556
+ // Defer instance materialization: hooks / context reads call
557
+ // materializeInstance() lazily. A component that touches neither never
558
+ // allocates or registers an instance.
559
+ hookRenderState.currentRuntime = runtime;
560
+ hookRenderState.currentInstance = undefined;
561
+ hookRenderState.pendingInstance = { runtime, path, owner, existing };
562
+
563
+ try {
564
+ return withContextReadObserver(recordContextDependency, render);
565
+ } finally {
566
+ hookRenderState.currentRuntime = previousRuntime;
567
+ hookRenderState.currentInstance = previousInstance;
568
+ hookRenderState.pendingInstance = previousPending;
569
+ }
570
+ }
571
+
572
+ // Materialize the deferred instance for the rendering component the first time
573
+ // a hook or context read needs it.
574
+ function materializeInstance(): ComponentInstance {
575
+ const pending = hookRenderState.pendingInstance;
576
+ if (pending === undefined) {
577
+ throw new Error("Hooks can only be called while rendering.");
578
+ }
579
+
580
+ const { runtime, path, owner } = pending;
581
+ let instance = pending.existing;
582
+ if (instance === undefined) {
583
+ instance = {
584
+ owner,
585
+ path,
586
+ hooks: [],
587
+ hookIndex: 0,
588
+ dirty: false,
589
+ devToolsHookSuppressionDepth: 0,
590
+ };
591
+ runtime.instances.set(path, instance);
592
+ indexInstanceKey(runtime, path);
593
+ } else {
594
+ instance.owner = owner;
595
+ }
524
596
  runtime.activeInstanceKeys?.add(path);
525
597
  instance.hookIndex = 0;
526
598
  instance.dirty = false;
@@ -534,17 +606,17 @@ export function renderWithRootRuntime<T>(
534
606
  delete instance.devToolsHookTypes;
535
607
  }
536
608
  instance.devToolsHookSuppressionDepth = 0;
537
- hookRenderState.currentRuntime = runtime;
538
609
  hookRenderState.currentInstance = instance;
610
+ hookRenderState.pendingInstance = undefined;
611
+ return instance;
612
+ }
539
613
 
540
- try {
541
- return withContextReadObserver((context, value) => {
542
- (instance.contextDependencies ??= new Map()).set(context, value);
543
- }, render);
544
- } finally {
545
- hookRenderState.currentRuntime = previousRuntime;
546
- hookRenderState.currentInstance = previousInstance;
547
- }
614
+ function recordContextDependency(
615
+ context: ReactCompatContextLike<unknown>,
616
+ value: unknown,
617
+ ): void {
618
+ const instance = hookRenderState.currentInstance ?? materializeInstance();
619
+ (instance.contextDependencies ??= new Map()).set(context, value);
548
620
  }
549
621
 
550
622
  export function hasChangedContextDependency(
@@ -575,22 +647,27 @@ export function hasContextDependency(
575
647
  return keys.some((key) => runtime.instances.get(key)?.contextDependencies !== undefined);
576
648
  }
577
649
 
650
+ // Shared read-only empty result so components with no registered instances
651
+ // (e.g. hookless rows under lazy instance materialization) don't each allocate
652
+ // a fresh array. Callers treat instance-key lists as read-only.
653
+ const EMPTY_INSTANCE_KEYS: string[] = [];
654
+
578
655
  export function collectRuntimeInstanceKeys(runtime: RootRuntime, prefix: string): string[] {
579
656
  const keys = runtime.instanceKeysByPrefix.get(prefix);
580
657
 
581
658
  if (keys === undefined) {
582
- return [];
659
+ return EMPTY_INSTANCE_KEYS;
583
660
  }
584
661
 
585
- const activeKeys: string[] = [];
662
+ let activeKeys: string[] | undefined;
586
663
 
587
664
  for (const key of keys) {
588
665
  if (runtime.instances.has(key)) {
589
- activeKeys.push(key);
666
+ (activeKeys ??= []).push(key);
590
667
  }
591
668
  }
592
669
 
593
- return activeKeys;
670
+ return activeKeys ?? EMPTY_INSTANCE_KEYS;
594
671
  }
595
672
 
596
673
  export function getDevToolsHookState(
@@ -787,11 +864,11 @@ export function useState<T>(
787
864
  throw new Error("Hook order changed between renders.");
788
865
  }
789
866
 
790
- const setState = (value: T | ((previous: T) => T)): void => {
867
+ slot.dispatch ??= (value: unknown): void => {
791
868
  const previousValue = slot.value;
792
869
  const nextValue =
793
870
  typeof value === "function"
794
- ? (value as (previous: T) => T)(slot.value as T)
871
+ ? (value as (previous: unknown) => unknown)(slot.value)
795
872
  : value;
796
873
 
797
874
  if (Object.is(slot.value, nextValue)) {
@@ -813,7 +890,17 @@ export function useState<T>(
813
890
  optionsAllowDirectTextBinding(value) &&
814
891
  updateDirectTextBinding(slot.textBinding, nextValue);
815
892
 
816
- if (canUseDirectTextBinding) {
893
+ const canUseDirectStateBinding =
894
+ hookRenderState.hostCommitDepth === 0 &&
895
+ hookRenderState.currentRuntime !== runtime &&
896
+ hookRenderState.currentInstance !== instance &&
897
+ runtime.effectFlushPhase === undefined &&
898
+ eventBatchDepth === 0 &&
899
+ transitionDepth === 0 &&
900
+ optionsAllowDirectTextBinding(value) &&
901
+ updateDirectStateBinding(slot.stateBinding, nextValue);
902
+
903
+ if (canUseDirectTextBinding || canUseDirectStateBinding) {
817
904
  return;
818
905
  }
819
906
 
@@ -825,6 +912,7 @@ export function useState<T>(
825
912
 
826
913
  scheduleInstanceUpdate(runtime, instance, { deferSync: typeof value === "function" });
827
914
  };
915
+ const setState = slot.dispatch as (value: T | ((previous: T) => T)) => void;
828
916
 
829
917
  recordDevToolsHook("useState", {
830
918
  kind: "state",
@@ -836,6 +924,7 @@ export function useState<T>(
836
924
  (value: T | ((previous: T) => T)) => void,
837
925
  ] & Record<PropertyKey, unknown>;
838
926
  result[REACTIVE_TEXT_BINDING_META] = getStateTextBinding(slot);
927
+ result[REACTIVE_STATE_BINDING_META] = getStateBinding(slot);
839
928
  return result;
840
929
  }
841
930
 
@@ -884,6 +973,19 @@ function getStateTextBinding(slot: Extract<HookSlot, { kind: "state" }>): Reacti
884
973
  return slot.textBinding;
885
974
  }
886
975
 
976
+ function getStateBinding(slot: Extract<HookSlot, { kind: "state" }>): ReactiveStateBinding {
977
+ slot.stateBinding ??= {
978
+ value: slot.value,
979
+ source: { subscribers: null },
980
+ get() {
981
+ trackSource(this.source);
982
+ return this.value;
983
+ },
984
+ };
985
+ slot.stateBinding.value = slot.value;
986
+ return slot.stateBinding;
987
+ }
988
+
887
989
  function optionsAllowDirectTextBinding(value: unknown): boolean {
888
990
  return typeof value !== "function";
889
991
  }
@@ -913,6 +1015,20 @@ function updateDirectTextBinding(binding: ReactiveTextBinding | undefined, value
913
1015
  return updated;
914
1016
  }
915
1017
 
1018
+ function updateDirectStateBinding(
1019
+ binding: ReactiveStateBinding | undefined,
1020
+ value: unknown,
1021
+ ): boolean {
1022
+ if (binding === undefined || binding.source.subscribers === null) {
1023
+ return false;
1024
+ }
1025
+
1026
+ binding.value = value;
1027
+ notifySubscribers(binding.source);
1028
+ flushQueuedComputations();
1029
+ return true;
1030
+ }
1031
+
916
1032
  function isReactiveTextBinding(value: unknown): value is ReactiveTextBinding {
917
1033
  return (
918
1034
  typeof value === "object" &&
@@ -928,11 +1044,12 @@ export function useReducer<TState, TAction, TInitial = TState>(
928
1044
  initialArg: TInitial,
929
1045
  init?: (initialArg: TInitial) => TState,
930
1046
  ): [TState, (action: TAction) => void] {
931
- const [state, setState] = runWithoutDevToolsHookTracking(() =>
1047
+ const stateTuple = runWithoutDevToolsHookTracking(() =>
932
1048
  useState<TState>(() =>
933
1049
  init === undefined ? (initialArg as unknown as TState) : init(initialArg),
934
1050
  ),
935
1051
  );
1052
+ const [state, setState] = stateTuple;
936
1053
  const reducerRef = runWithoutDevToolsHookTracking(() => useRef(reducer));
937
1054
  const stateRef = runWithoutDevToolsHookTracking(() => useRef(state));
938
1055
  const dispatchRef = runWithoutDevToolsHookTracking(() =>
@@ -956,7 +1073,18 @@ export function useReducer<TState, TAction, TInitial = TState>(
956
1073
  value: state,
957
1074
  });
958
1075
 
959
- return [state, dispatchRef.current];
1076
+ const result = [state, dispatchRef.current] as [
1077
+ TState,
1078
+ (action: TAction) => void,
1079
+ ] & Record<PropertyKey, unknown>;
1080
+ const stateBinding = (stateTuple as unknown as Record<PropertyKey, unknown>)[
1081
+ REACTIVE_STATE_BINDING_META
1082
+ ];
1083
+
1084
+ if (stateBinding !== undefined) {
1085
+ result[REACTIVE_STATE_BINDING_META] = stateBinding;
1086
+ }
1087
+ return result;
960
1088
  }
961
1089
 
962
1090
  /** Returns a stable mutable ref object for the component instance. */
@@ -1300,6 +1428,7 @@ export function useSyncExternalStore<T>(
1300
1428
  if (slot.kind !== "store") {
1301
1429
  throw new Error("Hook order changed between renders.");
1302
1430
  }
1431
+ slot.getSnapshot = getSnapshot as () => unknown;
1303
1432
 
1304
1433
  const isHydrationMount =
1305
1434
  runtime.idMode === "server" && slot.hasMounted !== true && getServerSnapshot !== undefined;
@@ -1319,7 +1448,7 @@ export function useSyncExternalStore<T>(
1319
1448
  return;
1320
1449
  }
1321
1450
 
1322
- const nextSnapshot = getSnapshot();
1451
+ const nextSnapshot = (slot.getSnapshot ?? getSnapshot)();
1323
1452
 
1324
1453
  if (!Object.is(slot.value, nextSnapshot)) {
1325
1454
  if (hookRenderState.hostCommitDepth > 0 && !Object.hasOwn(slot, "hostCommitValue")) {
@@ -1353,7 +1482,7 @@ export function useSyncExternalStore<T>(
1353
1482
  return () => {
1354
1483
  unsubscribe();
1355
1484
  };
1356
- }, [subscribe, getSnapshot]));
1485
+ }, [subscribe]));
1357
1486
 
1358
1487
  recordDevToolsHook("useSyncExternalStore", {
1359
1488
  kind: "store",
@@ -2044,15 +2173,16 @@ function scheduleInstanceUpdate(
2044
2173
  scheduleRuntimeRerender(runtime, options);
2045
2174
  }
2046
2175
 
2047
- function flushHostCommitRerenders(): void {
2176
+ function flushHostCommitRerenders(): boolean {
2048
2177
  if (
2049
2178
  hostCommitRerenderDepth > 0 ||
2050
2179
  hookRenderState.hostCommitDepth > 0 ||
2051
2180
  hookRenderState.queuedHostCommitRerenders.size === 0
2052
2181
  ) {
2053
- return;
2182
+ return false;
2054
2183
  }
2055
2184
 
2185
+ let didRerender = false;
2056
2186
  hostCommitRerenderDepth += 1;
2057
2187
  try {
2058
2188
  for (
@@ -2069,6 +2199,7 @@ function flushHostCommitRerenders(): void {
2069
2199
  clearHostCommitStateBaselines(runtime);
2070
2200
 
2071
2201
  if (hasDirtyInstance) {
2202
+ didRerender = true;
2072
2203
  runtime.rerender("sync");
2073
2204
  }
2074
2205
  }
@@ -2077,6 +2208,20 @@ function flushHostCommitRerenders(): void {
2077
2208
  } finally {
2078
2209
  hostCommitRerenderDepth -= 1;
2079
2210
  }
2211
+ return didRerender;
2212
+ }
2213
+
2214
+ function dedupePendingEffects(queue: PendingEffect[]): void {
2215
+ if (queue.length < 2) {
2216
+ return;
2217
+ }
2218
+
2219
+ const latestBySlot = new Map<Extract<HookSlot, { kind: "effect" }>, PendingEffect>();
2220
+ for (const effect of queue) {
2221
+ latestBySlot.set(effect.slot, effect);
2222
+ }
2223
+ queue.length = 0;
2224
+ queue.push(...latestBySlot.values());
2080
2225
  }
2081
2226
 
2082
2227
  function flushEffectFlushRerenders(): void {
@@ -2299,11 +2444,7 @@ function requireRuntime(): RootRuntime {
2299
2444
  }
2300
2445
 
2301
2446
  function requireInstance(): ComponentInstance {
2302
- if (hookRenderState.currentInstance === undefined) {
2303
- throw new Error("Hooks can only be called while rendering.");
2304
- }
2305
-
2306
- return hookRenderState.currentInstance;
2447
+ return hookRenderState.currentInstance ?? materializeInstance();
2307
2448
  }
2308
2449
 
2309
2450
  function areHookInputsEqual(