@reckona/mreact-compat 0.0.144 → 0.0.146

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.
package/src/element.ts CHANGED
@@ -15,6 +15,9 @@ export const HOST_OWN_PROPS_META = Symbol.for("modular.react.host_own_props_meta
15
15
  export const HOST_CHILDREN_ONLY_PROPS_META = Symbol.for(
16
16
  "modular.react.host_children_only_props_meta",
17
17
  );
18
+ export const REACTIVE_TEXT_BINDING_META = Symbol.for(
19
+ "modular.react.reactive_text_binding_meta",
20
+ );
18
21
  const hasOwnProperty = Object.prototype.hasOwnProperty;
19
22
 
20
23
  export interface ReactCompatProviderType {
@@ -274,18 +277,19 @@ function copyElementProps(
274
277
  base?: Record<string, unknown>,
275
278
  omitChildren = false,
276
279
  ): Record<string, unknown> {
277
- const props: Record<string, unknown> = {};
280
+ const props: Record<PropertyKey, unknown> = {};
278
281
 
279
282
  if (base !== undefined) {
280
283
  copyOwnStringElementProps(base, props, omitChildren);
281
284
  }
282
285
 
283
286
  if (source === null || source === undefined) {
284
- return props;
287
+ return props as Record<string, unknown>;
285
288
  }
286
289
 
287
290
  copyOwnStringElementProps(source, props, omitChildren);
288
- return props;
291
+ copyOwnSymbolElementProps(source, props);
292
+ return props as Record<string, unknown>;
289
293
  }
290
294
 
291
295
  function copyOwnStringElementProps(
@@ -310,6 +314,16 @@ function copyOwnStringElementProps(
310
314
  }
311
315
  }
312
316
 
317
+ function copyOwnSymbolElementProps(
318
+ source: Record<string, unknown>,
319
+ target: Record<PropertyKey, unknown>,
320
+ ): void {
321
+ const symbolSource = source as Record<PropertyKey, unknown>;
322
+ for (const symbol of Object.getOwnPropertySymbols(source)) {
323
+ target[symbol] = symbolSource[symbol];
324
+ }
325
+ }
326
+
313
327
  function normalizeElementType<P>(type: ElementType<P>): ElementType<P> {
314
328
  return isReactCompatContextProviderShorthand(type) ? (type.Provider as ElementType<P>) : type;
315
329
  }
package/src/hooks.ts CHANGED
@@ -11,6 +11,7 @@ import {
11
11
  useContext,
12
12
  withContextReadObserver,
13
13
  } from "./context.js";
14
+ import { REACTIVE_TEXT_BINDING_META } from "./element.js";
14
15
  import { isThenable } from "./thenable.js";
15
16
 
16
17
  export interface RootRuntime {
@@ -124,7 +125,7 @@ export type DevToolsHookValue =
124
125
  | { kind: "effect"; effectKind: "insertion" | "layout" | "normal"; deps?: readonly unknown[] };
125
126
 
126
127
  type HookSlot =
127
- | { kind: "state"; value: unknown; hostCommitValue?: unknown }
128
+ | { kind: "state"; value: unknown; hostCommitValue?: unknown; textBinding?: ReactiveTextBinding }
128
129
  | {
129
130
  kind: "action-state";
130
131
  state: unknown;
@@ -195,6 +196,14 @@ const queuedTransitionRerenders = new Map<RootRuntime, TransitionContext>();
195
196
  const queuedEventRerenders = new Set<RootRuntime>();
196
197
  export const version = "19.2.6";
197
198
 
199
+ export interface ReactiveTextBinding {
200
+ value: unknown;
201
+ subscribers: Set<Text>;
202
+ }
203
+
204
+ const reactiveTextBindingsByNode = new WeakMap<Text, ReactiveTextBinding>();
205
+ const hydratedIdsByRuntime = new WeakMap<RootRuntime, Map<string, string>>();
206
+
198
207
  export function act<T>(callback: () => T): T extends PromiseLike<unknown> ? Promise<void> : void {
199
208
  const previousPriority = currentEventPriority;
200
209
  currentEventPriority = "discrete";
@@ -778,6 +787,20 @@ export function useState<T>(
778
787
  }
779
788
 
780
789
  slot.value = nextValue;
790
+ const canUseDirectTextBinding =
791
+ hookRenderState.hostCommitDepth === 0 &&
792
+ hookRenderState.currentRuntime !== runtime &&
793
+ hookRenderState.currentInstance !== instance &&
794
+ runtime.effectFlushPhase === undefined &&
795
+ eventBatchDepth === 0 &&
796
+ transitionDepth === 0 &&
797
+ optionsAllowDirectTextBinding(value) &&
798
+ updateDirectTextBinding(slot.textBinding, nextValue);
799
+
800
+ if (canUseDirectTextBinding) {
801
+ return;
802
+ }
803
+
781
804
  if (hookRenderState.hostCommitDepth > 0) {
782
805
  updateHostCommitDirtyState(instance);
783
806
  hookRenderState.queuedHostCommitRerenders.add(runtime);
@@ -792,7 +815,86 @@ export function useState<T>(
792
815
  value: slot.value,
793
816
  });
794
817
 
795
- return [slot.value as T, setState];
818
+ const result = [slot.value as T, setState] as [
819
+ T,
820
+ (value: T | ((previous: T) => T)) => void,
821
+ ] & Record<PropertyKey, unknown>;
822
+ result[REACTIVE_TEXT_BINDING_META] = getStateTextBinding(slot);
823
+ return result;
824
+ }
825
+
826
+ export function subscribeReactiveTextBinding(binding: unknown, node: Text): void {
827
+ if (!isReactiveTextBinding(binding)) {
828
+ clearReactiveTextBinding(node);
829
+ return;
830
+ }
831
+
832
+ const previous = reactiveTextBindingsByNode.get(node);
833
+
834
+ if (previous !== undefined && previous !== binding) {
835
+ previous.subscribers.delete(node);
836
+ }
837
+
838
+ reactiveTextBindingsByNode.set(node, binding);
839
+ binding.subscribers.add(node);
840
+ }
841
+
842
+ function clearReactiveTextBinding(node: Text): void {
843
+ const previous = reactiveTextBindingsByNode.get(node);
844
+
845
+ if (previous === undefined) {
846
+ return;
847
+ }
848
+
849
+ previous.subscribers.delete(node);
850
+ reactiveTextBindingsByNode.delete(node);
851
+ }
852
+
853
+ function getStateTextBinding(slot: Extract<HookSlot, { kind: "state" }>): ReactiveTextBinding {
854
+ slot.textBinding ??= {
855
+ value: slot.value,
856
+ subscribers: new Set(),
857
+ };
858
+ slot.textBinding.value = slot.value;
859
+ return slot.textBinding;
860
+ }
861
+
862
+ function optionsAllowDirectTextBinding(value: unknown): boolean {
863
+ return typeof value !== "function";
864
+ }
865
+
866
+ function updateDirectTextBinding(binding: ReactiveTextBinding | undefined, value: unknown): boolean {
867
+ if (binding === undefined || binding.subscribers.size === 0) {
868
+ return false;
869
+ }
870
+
871
+ let updated = false;
872
+ const nextText = String(value);
873
+
874
+ for (const node of binding.subscribers) {
875
+ if (node.parentNode === null) {
876
+ binding.subscribers.delete(node);
877
+ reactiveTextBindingsByNode.delete(node);
878
+ continue;
879
+ }
880
+
881
+ if (node.data !== nextText) {
882
+ node.data = nextText;
883
+ }
884
+ updated = true;
885
+ }
886
+
887
+ binding.value = value;
888
+ return updated;
889
+ }
890
+
891
+ function isReactiveTextBinding(value: unknown): value is ReactiveTextBinding {
892
+ return (
893
+ typeof value === "object" &&
894
+ value !== null &&
895
+ "subscribers" in value &&
896
+ (value as { subscribers?: unknown }).subscribers instanceof Set
897
+ );
796
898
  }
797
899
 
798
900
  export function useReducer<TState, TAction, TInitial = TState>(
@@ -853,14 +955,30 @@ export function useRef<T>(initial: T): { current: T } {
853
955
 
854
956
  export function useId(): string {
855
957
  const runtime = requireRuntime();
958
+ const instance = requireInstance();
959
+ const idSlotKey = `${instance.path}:${instance.hookIndex}`;
856
960
  const idRef = runWithoutDevToolsHookTracking(() =>
857
961
  useRef<string | undefined>(undefined)
858
962
  );
859
963
 
860
964
  if (idRef.current === undefined) {
861
- const mode = runtime.idMode === "server" ? "R" : "r";
862
- idRef.current = `_${runtime.identifierPrefix}${mode}_${runtime.idCounter}_`;
863
- runtime.idCounter += 1;
965
+ const hydratedId = runtime.idMode === "client"
966
+ ? hydratedIdsByRuntime.get(runtime)?.get(idSlotKey)
967
+ : undefined;
968
+
969
+ if (hydratedId !== undefined) {
970
+ idRef.current = hydratedId;
971
+ } else {
972
+ const mode = runtime.idMode === "server" ? "R" : "r";
973
+ idRef.current = `_${runtime.identifierPrefix}${mode}_${runtime.idCounter}_`;
974
+ runtime.idCounter += 1;
975
+
976
+ if (runtime.idMode === "server") {
977
+ const hydratedIds = hydratedIdsByRuntime.get(runtime) ?? new Map<string, string>();
978
+ hydratedIds.set(idSlotKey, idRef.current);
979
+ hydratedIdsByRuntime.set(runtime, hydratedIds);
980
+ }
981
+ }
864
982
  }
865
983
 
866
984
  recordDevToolsHook("useId", {
@@ -1114,7 +1232,7 @@ export function useLayoutEffect(
1114
1232
  export function useSyncExternalStore<T>(
1115
1233
  subscribe: (listener: () => void) => () => void,
1116
1234
  getSnapshot: () => T,
1117
- getServerSnapshot: () => T = getSnapshot,
1235
+ getServerSnapshot?: () => T,
1118
1236
  ): T {
1119
1237
  const runtime = requireRuntime();
1120
1238
  const instance = requireInstance();
@@ -1123,7 +1241,12 @@ export function useSyncExternalStore<T>(
1123
1241
  let slot = instance.hooks[index];
1124
1242
 
1125
1243
  if (slot === undefined) {
1126
- slot = { kind: "store", value: getServerSnapshot() };
1244
+ slot = {
1245
+ kind: "store",
1246
+ value: runtime.idMode === "server" && getServerSnapshot !== undefined
1247
+ ? getServerSnapshot()
1248
+ : getSnapshot(),
1249
+ };
1127
1250
  instance.hooks[index] = slot;
1128
1251
  }
1129
1252
 
@@ -1131,13 +1254,17 @@ export function useSyncExternalStore<T>(
1131
1254
  throw new Error("Hook order changed between renders.");
1132
1255
  }
1133
1256
 
1134
- const currentSnapshot = getSnapshot();
1257
+ const isHydrationMount =
1258
+ runtime.idMode === "server" && slot.hasMounted !== true && getServerSnapshot !== undefined;
1259
+ const currentSnapshot = isHydrationMount ? slot.value as T : getSnapshot();
1135
1260
 
1136
1261
  if (!Object.is(slot.value, currentSnapshot)) {
1137
1262
  slot.value = currentSnapshot;
1138
1263
  }
1139
1264
 
1140
- recordExternalStoreCheck(getSnapshot, currentSnapshot);
1265
+ if (!isHydrationMount) {
1266
+ recordExternalStoreCheck(getSnapshot, currentSnapshot);
1267
+ }
1141
1268
 
1142
1269
  runWithoutDevToolsHookTracking(() => useEffect(() => {
1143
1270
  const checkForUpdates = (): void => {
@@ -8,6 +8,7 @@ import {
8
8
  LAZY_TYPE,
9
9
  MEMO_TYPE,
10
10
  Profiler,
11
+ REACTIVE_TEXT_BINDING_META,
11
12
  STRICT_MODE_TYPE,
12
13
  Suspense,
13
14
  SuspenseList,
@@ -49,6 +50,7 @@ import {
49
50
  collectRuntimeInstanceKeys,
50
51
  hasContextDependency,
51
52
  hasChangedContextDependency,
53
+ subscribeReactiveTextBinding,
52
54
  type RootRuntime,
53
55
  } from "./hooks.js";
54
56
  import { isThenable } from "./thenable.js";
@@ -1653,14 +1655,13 @@ function commitHostDirtyFiber(
1653
1655
  }
1654
1656
 
1655
1657
  if (directTextChild !== undefined) {
1656
- syncDirectHostTextChild(element, directTextChild);
1658
+ const text = syncDirectHostTextChild(element, directTextChild);
1659
+ subscribeReactiveHostTextBinding(props, text);
1657
1660
  } else if (fiber.subtreeFlags !== NoFlags) {
1658
1661
  commitHostDirtyChildren(fiber.child, element, eventRoot, `${path}.c`, options);
1659
1662
  }
1660
1663
 
1661
- if (!propsAreUnchanged && !propsAreChildrenOnly && !textOnlyRowUpdate) {
1662
- applyPostChildFormProps(element, props);
1663
- }
1664
+ applyPostChildFormProps(element, props, previousProps);
1664
1665
  fiber.memoizedProps = props;
1665
1666
  finishCommittedFiber(fiber);
1666
1667
  return;
@@ -2043,7 +2044,8 @@ function commitHostFiber(
2043
2044
  applyChangedRef(previousProps?.ref, props.ref, element);
2044
2045
  }
2045
2046
  if (directTextChild !== undefined) {
2046
- syncDirectHostTextChild(element, directTextChild);
2047
+ const text = syncDirectHostTextChild(element, directTextChild);
2048
+ subscribeReactiveHostTextBinding(props, text);
2047
2049
  } else if (
2048
2050
  fiber.hostChildListChanged ||
2049
2051
  fiber.childListChanged ||
@@ -2061,9 +2063,7 @@ function commitHostFiber(
2061
2063
  commitHostChildren(fiber.child, element, eventRoot, `${path}.c`, options);
2062
2064
  }
2063
2065
 
2064
- if (!propsAreUnchanged && !propsAreChildrenOnly && !textOnlyRowUpdate) {
2065
- applyPostChildFormProps(element, props);
2066
- }
2066
+ applyPostChildFormProps(element, props, previousProps);
2067
2067
  fiber.memoizedProps = props;
2068
2068
  finishCommittedFiber(fiber);
2069
2069
  return [element];
@@ -2410,17 +2410,36 @@ function shouldUseDirectHostTextChild(): boolean {
2410
2410
  return globalProcess?.env?.NODE_ENV === "production";
2411
2411
  }
2412
2412
 
2413
- function syncDirectHostTextChild(element: Element, text: string): void {
2413
+ function syncDirectHostTextChild(element: Element, text: string): Text {
2414
2414
  const firstChild = element.firstChild;
2415
2415
 
2416
2416
  if (firstChild instanceof Text && firstChild.nextSibling === null) {
2417
2417
  if (firstChild.data !== text) {
2418
2418
  firstChild.data = text;
2419
2419
  }
2420
- return;
2420
+ return firstChild;
2421
2421
  }
2422
2422
 
2423
2423
  element.textContent = text;
2424
+ const nextFirstChild = element.firstChild;
2425
+
2426
+ if (!(nextFirstChild instanceof Text)) {
2427
+ const textNode = document.createTextNode(text);
2428
+ element.replaceChildren(textNode);
2429
+ return textNode;
2430
+ }
2431
+
2432
+ return nextFirstChild;
2433
+ }
2434
+
2435
+ function subscribeReactiveHostTextBinding(
2436
+ props: Record<string, unknown>,
2437
+ text: Text,
2438
+ ): void {
2439
+ subscribeReactiveTextBinding(
2440
+ (props as Record<PropertyKey, unknown>)[REACTIVE_TEXT_BINDING_META],
2441
+ text,
2442
+ );
2424
2443
  }
2425
2444
 
2426
2445
  function shouldPreserveContentEditableChildren(
@@ -1,4 +1,8 @@
1
- import { Fragment, jsx } from "./jsx-runtime.js";
1
+ import {
2
+ Fragment,
3
+ REACTIVE_TEXT_BINDING_META,
4
+ jsx,
5
+ } from "./jsx-runtime.js";
2
6
  import type {
3
7
  ElementType,
4
8
  ReactCompatElement,
@@ -10,6 +14,7 @@ import type {
10
14
  } from "./jsx-runtime.js";
11
15
 
12
16
  export { Fragment };
17
+ export { REACTIVE_TEXT_BINDING_META };
13
18
  export type {
14
19
  FormEvent,
15
20
  FormEventHandler,
@@ -1,4 +1,8 @@
1
- import { createElementFromJsxConfig, Fragment } from "./element.js";
1
+ import {
2
+ createElementFromJsxConfig,
3
+ Fragment,
4
+ REACTIVE_TEXT_BINDING_META,
5
+ } from "./element.js";
2
6
  import type {
3
7
  ElementType,
4
8
  ReactCompatElement,
@@ -6,6 +10,7 @@ import type {
6
10
  } from "./element.js";
7
11
 
8
12
  export { Fragment };
13
+ export { REACTIVE_TEXT_BINDING_META };
9
14
 
10
15
  export type JSXEvent<
11
16
  TCurrentTarget extends EventTarget,
package/src/reconciler.ts CHANGED
@@ -116,6 +116,7 @@ export function renderIntoContainer(
116
116
  scope.before?.parentNode?.removeChild(scope.before);
117
117
  scope.after?.parentNode?.removeChild(scope.after);
118
118
  }
119
+ runtime.idMode = "client";
119
120
  committed = true;
120
121
  } finally {
121
122
  runtime.endRender(committed);
package/src/root.ts CHANGED
@@ -159,6 +159,7 @@ function renderHostFiberIntoContainer(
159
159
  collectPortalNodes(fiberRoot.current, runtime);
160
160
  removeStalePortalNodes(portalSnapshot, runtime);
161
161
  commitDevToolsRoot(container, fiberRoot);
162
+ runtime.idMode = "client";
162
163
  committed = true;
163
164
  return finishedWork;
164
165
  } finally {
@@ -218,6 +219,7 @@ function renderHydratingHostFiberIntoContainer(
218
219
  collectPortalNodes(fiberRoot.current, runtime);
219
220
  removeStalePortalNodes(portalSnapshot, runtime);
220
221
  commitDevToolsRoot(container, fiberRoot);
222
+ runtime.idMode = "client";
221
223
  committed = true;
222
224
  return finishedWork;
223
225
  } finally {
@@ -266,17 +268,33 @@ export function hydrateRoot(
266
268
  const runtime = createRootRuntime((priority = "sync") => {
267
269
  if (runtime.currentElement !== undefined) {
268
270
  enqueueRootRender(fiberRoot, runtime.currentElement, laneForRenderPriority(priority), () => {
271
+ const useHydratingRerender =
272
+ runtime.idMode === "server" ||
273
+ renderOptions.resumeId !== undefined ||
274
+ renderOptions.consumeResumeMarkers !== undefined;
269
275
  if (canRenderHostFiber(runtime.currentElement as ReactCompatNode)) {
270
- return renderHydratingHostFiberIntoContainer(
271
- container,
272
- fiberRoot,
273
- runtime,
274
- runtime.currentElement as ReactCompatNode,
275
- renderOptions,
276
- );
276
+ return useHydratingRerender
277
+ ? renderHydratingHostFiberIntoContainer(
278
+ container,
279
+ fiberRoot,
280
+ runtime,
281
+ runtime.currentElement as ReactCompatNode,
282
+ renderOptions,
283
+ )
284
+ : renderHostFiberIntoContainer(
285
+ container,
286
+ fiberRoot,
287
+ runtime,
288
+ runtime.currentElement as ReactCompatNode,
289
+ );
277
290
  }
278
291
 
279
- renderIntoContainer(container, runtime.currentElement, runtime, renderOptions);
292
+ renderIntoContainer(
293
+ container,
294
+ runtime.currentElement,
295
+ runtime,
296
+ useHydratingRerender ? renderOptions : {},
297
+ );
280
298
  });
281
299
  }
282
300
  }, {