@reckona/mreact-compat 0.0.170 → 0.0.172
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/devtools.js +2 -0
- package/dist/devtools.js.map +1 -1
- package/dist/dom-host-rules.d.ts.map +1 -1
- package/dist/dom-host-rules.js +4 -1
- package/dist/dom-host-rules.js.map +1 -1
- package/dist/dom-props.js +2 -1
- package/dist/dom-props.js.map +1 -1
- package/dist/element.d.ts +13 -1
- package/dist/element.d.ts.map +1 -1
- package/dist/element.js +30 -2
- package/dist/element.js.map +1 -1
- package/dist/fiber-commit.js +2 -1
- package/dist/fiber-commit.js.map +1 -1
- package/dist/fiber-host.d.ts +1 -1
- package/dist/fiber-host.d.ts.map +1 -1
- package/dist/fiber-host.js +1 -1
- package/dist/fiber-host.js.map +1 -1
- package/dist/fiber.d.ts +2 -1
- package/dist/fiber.d.ts.map +1 -1
- package/dist/fiber.js +2 -0
- package/dist/fiber.js.map +1 -1
- package/dist/hooks.d.ts +7 -0
- package/dist/hooks.d.ts.map +1 -1
- package/dist/hooks.js +122 -41
- package/dist/hooks.js.map +1 -1
- package/dist/host-reconciler.d.ts +1 -0
- package/dist/host-reconciler.d.ts.map +1 -1
- package/dist/host-reconciler.js +659 -50
- package/dist/host-reconciler.js.map +1 -1
- package/dist/jsx-dev-runtime.d.ts +3 -1
- package/dist/jsx-dev-runtime.d.ts.map +1 -1
- package/dist/jsx-dev-runtime.js +3 -1
- package/dist/jsx-dev-runtime.js.map +1 -1
- package/dist/jsx-runtime.d.ts +4 -2
- package/dist/jsx-runtime.d.ts.map +1 -1
- package/dist/jsx-runtime.js +9 -1
- package/dist/jsx-runtime.js.map +1 -1
- package/dist/reactive-prop-cell.d.ts +13 -0
- package/dist/reactive-prop-cell.d.ts.map +1 -0
- package/dist/reactive-prop-cell.js +36 -0
- package/dist/reactive-prop-cell.js.map +1 -0
- package/dist/root.d.ts.map +1 -1
- package/dist/root.js +3 -1
- package/dist/root.js.map +1 -1
- package/package.json +3 -3
- package/src/devtools.ts +2 -0
- package/src/dom-host-rules.ts +4 -1
- package/src/dom-props.ts +2 -1
- package/src/element.ts +60 -3
- package/src/fiber-commit.ts +3 -1
- package/src/fiber-host.ts +1 -0
- package/src/fiber.ts +4 -0
- package/src/hooks.ts +173 -41
- package/src/host-reconciler.ts +979 -64
- package/src/jsx-dev-runtime.ts +4 -0
- package/src/jsx-runtime.ts +16 -0
- package/src/reactive-prop-cell.ts +67 -0
- package/src/root.ts +3 -0
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 {
|
|
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,13 @@ export type DevToolsHookValue =
|
|
|
127
133
|
| { kind: "effect"; effectKind: "insertion" | "layout" | "normal"; deps?: readonly unknown[] };
|
|
128
134
|
|
|
129
135
|
type HookSlot =
|
|
130
|
-
| {
|
|
136
|
+
| {
|
|
137
|
+
kind: "state";
|
|
138
|
+
value: unknown;
|
|
139
|
+
hostCommitValue?: unknown;
|
|
140
|
+
textBinding?: ReactiveTextBinding;
|
|
141
|
+
stateBinding?: ReactiveStateBinding;
|
|
142
|
+
}
|
|
131
143
|
| {
|
|
132
144
|
kind: "action-state";
|
|
133
145
|
state: unknown;
|
|
@@ -158,9 +170,21 @@ type HookSlot =
|
|
|
158
170
|
strictReplay?: boolean;
|
|
159
171
|
};
|
|
160
172
|
|
|
173
|
+
interface PendingInstanceContext {
|
|
174
|
+
runtime: RootRuntime;
|
|
175
|
+
path: string;
|
|
176
|
+
owner: unknown;
|
|
177
|
+
existing: ComponentInstance | undefined;
|
|
178
|
+
}
|
|
179
|
+
|
|
161
180
|
interface HookRenderState {
|
|
162
181
|
currentRuntime: RootRuntime | undefined;
|
|
163
182
|
currentInstance: ComponentInstance | undefined;
|
|
183
|
+
// Deferred instance for the component currently rendering. The instance is
|
|
184
|
+
// only materialized (created, registered, prefix-indexed) when a hook or a
|
|
185
|
+
// context read first needs it, so components with no hooks/context (e.g. a
|
|
186
|
+
// pure host-rendering memo row) pay none of that per-render cost.
|
|
187
|
+
pendingInstance: PendingInstanceContext | undefined;
|
|
164
188
|
currentCacheScope: CacheScope | undefined;
|
|
165
189
|
hostCommitDepth: number;
|
|
166
190
|
queuedHostCommitRerenders: Set<RootRuntime>;
|
|
@@ -174,6 +198,7 @@ const hookRenderState =
|
|
|
174
198
|
] ??= {
|
|
175
199
|
currentRuntime: undefined,
|
|
176
200
|
currentInstance: undefined,
|
|
201
|
+
pendingInstance: undefined,
|
|
177
202
|
currentCacheScope: undefined,
|
|
178
203
|
hostCommitDepth: 0,
|
|
179
204
|
queuedHostCommitRerenders: new Set<RootRuntime>(),
|
|
@@ -204,6 +229,12 @@ export interface ReactiveTextBinding {
|
|
|
204
229
|
subscribers: Set<Text>;
|
|
205
230
|
}
|
|
206
231
|
|
|
232
|
+
export interface ReactiveStateBinding {
|
|
233
|
+
get(): unknown;
|
|
234
|
+
source: Source;
|
|
235
|
+
value: unknown;
|
|
236
|
+
}
|
|
237
|
+
|
|
207
238
|
const reactiveTextBindingsByNode = new WeakMap<Text, ReactiveTextBinding>();
|
|
208
239
|
const hydratedIdsByRuntime = new WeakMap<RootRuntime, Map<string, string>>();
|
|
209
240
|
|
|
@@ -378,6 +409,9 @@ export function createRootRuntime(
|
|
|
378
409
|
flushPendingEffects(this.pendingImperativeHandleEffects);
|
|
379
410
|
this.effectFlushPhase = "layout";
|
|
380
411
|
const strictLayoutEffects = flushPendingEffects(this.pendingLayoutEffects);
|
|
412
|
+
if (flushHostCommitRerenders()) {
|
|
413
|
+
dedupePendingEffects(this.pendingEffects);
|
|
414
|
+
}
|
|
381
415
|
this.effectFlushPhase = "normal";
|
|
382
416
|
const strictEffects = flushPendingEffects(this.pendingEffects);
|
|
383
417
|
this.effectFlushPhase = undefined;
|
|
@@ -502,25 +536,56 @@ export function renderWithRootRuntime<T>(
|
|
|
502
536
|
): T {
|
|
503
537
|
const previousRuntime = hookRenderState.currentRuntime;
|
|
504
538
|
const previousInstance = hookRenderState.currentInstance;
|
|
505
|
-
|
|
539
|
+
const previousPending = hookRenderState.pendingInstance;
|
|
506
540
|
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
541
|
+
let existing = runtime.instances.get(path);
|
|
542
|
+
if (existing !== undefined && owner !== undefined && existing.owner !== owner) {
|
|
543
|
+
cleanupInstance(existing);
|
|
544
|
+
runtime.instances.delete(path);
|
|
545
|
+
removeInstanceKeyFromIndex(runtime, path);
|
|
546
|
+
existing = undefined;
|
|
510
547
|
}
|
|
511
548
|
|
|
512
|
-
instance
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
549
|
+
// Defer instance materialization: hooks / context reads call
|
|
550
|
+
// materializeInstance() lazily. A component that touches neither never
|
|
551
|
+
// allocates or registers an instance.
|
|
552
|
+
hookRenderState.currentRuntime = runtime;
|
|
553
|
+
hookRenderState.currentInstance = undefined;
|
|
554
|
+
hookRenderState.pendingInstance = { runtime, path, owner, existing };
|
|
555
|
+
|
|
556
|
+
try {
|
|
557
|
+
return withContextReadObserver(recordContextDependency, render);
|
|
558
|
+
} finally {
|
|
559
|
+
hookRenderState.currentRuntime = previousRuntime;
|
|
560
|
+
hookRenderState.currentInstance = previousInstance;
|
|
561
|
+
hookRenderState.pendingInstance = previousPending;
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// Materialize the deferred instance for the rendering component the first time
|
|
566
|
+
// a hook or context read needs it.
|
|
567
|
+
function materializeInstance(): ComponentInstance {
|
|
568
|
+
const pending = hookRenderState.pendingInstance;
|
|
569
|
+
if (pending === undefined) {
|
|
570
|
+
throw new Error("Hooks can only be called while rendering.");
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
const { runtime, path, owner } = pending;
|
|
574
|
+
let instance = pending.existing;
|
|
575
|
+
if (instance === undefined) {
|
|
576
|
+
instance = {
|
|
577
|
+
owner,
|
|
578
|
+
path,
|
|
579
|
+
hooks: [],
|
|
580
|
+
hookIndex: 0,
|
|
581
|
+
dirty: false,
|
|
582
|
+
devToolsHookSuppressionDepth: 0,
|
|
583
|
+
};
|
|
584
|
+
runtime.instances.set(path, instance);
|
|
585
|
+
indexInstanceKey(runtime, path);
|
|
586
|
+
} else {
|
|
587
|
+
instance.owner = owner;
|
|
588
|
+
}
|
|
524
589
|
runtime.activeInstanceKeys?.add(path);
|
|
525
590
|
instance.hookIndex = 0;
|
|
526
591
|
instance.dirty = false;
|
|
@@ -534,17 +599,17 @@ export function renderWithRootRuntime<T>(
|
|
|
534
599
|
delete instance.devToolsHookTypes;
|
|
535
600
|
}
|
|
536
601
|
instance.devToolsHookSuppressionDepth = 0;
|
|
537
|
-
hookRenderState.currentRuntime = runtime;
|
|
538
602
|
hookRenderState.currentInstance = instance;
|
|
603
|
+
hookRenderState.pendingInstance = undefined;
|
|
604
|
+
return instance;
|
|
605
|
+
}
|
|
539
606
|
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
hookRenderState.currentInstance = previousInstance;
|
|
547
|
-
}
|
|
607
|
+
function recordContextDependency(
|
|
608
|
+
context: ReactCompatContextLike<unknown>,
|
|
609
|
+
value: unknown,
|
|
610
|
+
): void {
|
|
611
|
+
const instance = hookRenderState.currentInstance ?? materializeInstance();
|
|
612
|
+
(instance.contextDependencies ??= new Map()).set(context, value);
|
|
548
613
|
}
|
|
549
614
|
|
|
550
615
|
export function hasChangedContextDependency(
|
|
@@ -575,22 +640,27 @@ export function hasContextDependency(
|
|
|
575
640
|
return keys.some((key) => runtime.instances.get(key)?.contextDependencies !== undefined);
|
|
576
641
|
}
|
|
577
642
|
|
|
643
|
+
// Shared read-only empty result so components with no registered instances
|
|
644
|
+
// (e.g. hookless rows under lazy instance materialization) don't each allocate
|
|
645
|
+
// a fresh array. Callers treat instance-key lists as read-only.
|
|
646
|
+
const EMPTY_INSTANCE_KEYS: string[] = [];
|
|
647
|
+
|
|
578
648
|
export function collectRuntimeInstanceKeys(runtime: RootRuntime, prefix: string): string[] {
|
|
579
649
|
const keys = runtime.instanceKeysByPrefix.get(prefix);
|
|
580
650
|
|
|
581
651
|
if (keys === undefined) {
|
|
582
|
-
return
|
|
652
|
+
return EMPTY_INSTANCE_KEYS;
|
|
583
653
|
}
|
|
584
654
|
|
|
585
|
-
|
|
655
|
+
let activeKeys: string[] | undefined;
|
|
586
656
|
|
|
587
657
|
for (const key of keys) {
|
|
588
658
|
if (runtime.instances.has(key)) {
|
|
589
|
-
activeKeys.push(key);
|
|
659
|
+
(activeKeys ??= []).push(key);
|
|
590
660
|
}
|
|
591
661
|
}
|
|
592
662
|
|
|
593
|
-
return activeKeys;
|
|
663
|
+
return activeKeys ?? EMPTY_INSTANCE_KEYS;
|
|
594
664
|
}
|
|
595
665
|
|
|
596
666
|
export function getDevToolsHookState(
|
|
@@ -813,7 +883,17 @@ export function useState<T>(
|
|
|
813
883
|
optionsAllowDirectTextBinding(value) &&
|
|
814
884
|
updateDirectTextBinding(slot.textBinding, nextValue);
|
|
815
885
|
|
|
816
|
-
|
|
886
|
+
const canUseDirectStateBinding =
|
|
887
|
+
hookRenderState.hostCommitDepth === 0 &&
|
|
888
|
+
hookRenderState.currentRuntime !== runtime &&
|
|
889
|
+
hookRenderState.currentInstance !== instance &&
|
|
890
|
+
runtime.effectFlushPhase === undefined &&
|
|
891
|
+
eventBatchDepth === 0 &&
|
|
892
|
+
transitionDepth === 0 &&
|
|
893
|
+
optionsAllowDirectTextBinding(value) &&
|
|
894
|
+
updateDirectStateBinding(slot.stateBinding, nextValue);
|
|
895
|
+
|
|
896
|
+
if (canUseDirectTextBinding || canUseDirectStateBinding) {
|
|
817
897
|
return;
|
|
818
898
|
}
|
|
819
899
|
|
|
@@ -836,6 +916,7 @@ export function useState<T>(
|
|
|
836
916
|
(value: T | ((previous: T) => T)) => void,
|
|
837
917
|
] & Record<PropertyKey, unknown>;
|
|
838
918
|
result[REACTIVE_TEXT_BINDING_META] = getStateTextBinding(slot);
|
|
919
|
+
result[REACTIVE_STATE_BINDING_META] = getStateBinding(slot);
|
|
839
920
|
return result;
|
|
840
921
|
}
|
|
841
922
|
|
|
@@ -884,6 +965,19 @@ function getStateTextBinding(slot: Extract<HookSlot, { kind: "state" }>): Reacti
|
|
|
884
965
|
return slot.textBinding;
|
|
885
966
|
}
|
|
886
967
|
|
|
968
|
+
function getStateBinding(slot: Extract<HookSlot, { kind: "state" }>): ReactiveStateBinding {
|
|
969
|
+
slot.stateBinding ??= {
|
|
970
|
+
value: slot.value,
|
|
971
|
+
source: { subscribers: null },
|
|
972
|
+
get() {
|
|
973
|
+
trackSource(this.source);
|
|
974
|
+
return this.value;
|
|
975
|
+
},
|
|
976
|
+
};
|
|
977
|
+
slot.stateBinding.value = slot.value;
|
|
978
|
+
return slot.stateBinding;
|
|
979
|
+
}
|
|
980
|
+
|
|
887
981
|
function optionsAllowDirectTextBinding(value: unknown): boolean {
|
|
888
982
|
return typeof value !== "function";
|
|
889
983
|
}
|
|
@@ -913,6 +1007,20 @@ function updateDirectTextBinding(binding: ReactiveTextBinding | undefined, value
|
|
|
913
1007
|
return updated;
|
|
914
1008
|
}
|
|
915
1009
|
|
|
1010
|
+
function updateDirectStateBinding(
|
|
1011
|
+
binding: ReactiveStateBinding | undefined,
|
|
1012
|
+
value: unknown,
|
|
1013
|
+
): boolean {
|
|
1014
|
+
if (binding === undefined || binding.source.subscribers === null) {
|
|
1015
|
+
return false;
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
binding.value = value;
|
|
1019
|
+
notifySubscribers(binding.source);
|
|
1020
|
+
flushQueuedComputations();
|
|
1021
|
+
return true;
|
|
1022
|
+
}
|
|
1023
|
+
|
|
916
1024
|
function isReactiveTextBinding(value: unknown): value is ReactiveTextBinding {
|
|
917
1025
|
return (
|
|
918
1026
|
typeof value === "object" &&
|
|
@@ -928,11 +1036,12 @@ export function useReducer<TState, TAction, TInitial = TState>(
|
|
|
928
1036
|
initialArg: TInitial,
|
|
929
1037
|
init?: (initialArg: TInitial) => TState,
|
|
930
1038
|
): [TState, (action: TAction) => void] {
|
|
931
|
-
const
|
|
1039
|
+
const stateTuple = runWithoutDevToolsHookTracking(() =>
|
|
932
1040
|
useState<TState>(() =>
|
|
933
1041
|
init === undefined ? (initialArg as unknown as TState) : init(initialArg),
|
|
934
1042
|
),
|
|
935
1043
|
);
|
|
1044
|
+
const [state, setState] = stateTuple;
|
|
936
1045
|
const reducerRef = runWithoutDevToolsHookTracking(() => useRef(reducer));
|
|
937
1046
|
const stateRef = runWithoutDevToolsHookTracking(() => useRef(state));
|
|
938
1047
|
const dispatchRef = runWithoutDevToolsHookTracking(() =>
|
|
@@ -956,7 +1065,18 @@ export function useReducer<TState, TAction, TInitial = TState>(
|
|
|
956
1065
|
value: state,
|
|
957
1066
|
});
|
|
958
1067
|
|
|
959
|
-
|
|
1068
|
+
const result = [state, dispatchRef.current] as [
|
|
1069
|
+
TState,
|
|
1070
|
+
(action: TAction) => void,
|
|
1071
|
+
] & Record<PropertyKey, unknown>;
|
|
1072
|
+
const stateBinding = (stateTuple as unknown as Record<PropertyKey, unknown>)[
|
|
1073
|
+
REACTIVE_STATE_BINDING_META
|
|
1074
|
+
];
|
|
1075
|
+
|
|
1076
|
+
if (stateBinding !== undefined) {
|
|
1077
|
+
result[REACTIVE_STATE_BINDING_META] = stateBinding;
|
|
1078
|
+
}
|
|
1079
|
+
return result;
|
|
960
1080
|
}
|
|
961
1081
|
|
|
962
1082
|
/** Returns a stable mutable ref object for the component instance. */
|
|
@@ -2044,15 +2164,16 @@ function scheduleInstanceUpdate(
|
|
|
2044
2164
|
scheduleRuntimeRerender(runtime, options);
|
|
2045
2165
|
}
|
|
2046
2166
|
|
|
2047
|
-
function flushHostCommitRerenders():
|
|
2167
|
+
function flushHostCommitRerenders(): boolean {
|
|
2048
2168
|
if (
|
|
2049
2169
|
hostCommitRerenderDepth > 0 ||
|
|
2050
2170
|
hookRenderState.hostCommitDepth > 0 ||
|
|
2051
2171
|
hookRenderState.queuedHostCommitRerenders.size === 0
|
|
2052
2172
|
) {
|
|
2053
|
-
return;
|
|
2173
|
+
return false;
|
|
2054
2174
|
}
|
|
2055
2175
|
|
|
2176
|
+
let didRerender = false;
|
|
2056
2177
|
hostCommitRerenderDepth += 1;
|
|
2057
2178
|
try {
|
|
2058
2179
|
for (
|
|
@@ -2069,6 +2190,7 @@ function flushHostCommitRerenders(): void {
|
|
|
2069
2190
|
clearHostCommitStateBaselines(runtime);
|
|
2070
2191
|
|
|
2071
2192
|
if (hasDirtyInstance) {
|
|
2193
|
+
didRerender = true;
|
|
2072
2194
|
runtime.rerender("sync");
|
|
2073
2195
|
}
|
|
2074
2196
|
}
|
|
@@ -2077,6 +2199,20 @@ function flushHostCommitRerenders(): void {
|
|
|
2077
2199
|
} finally {
|
|
2078
2200
|
hostCommitRerenderDepth -= 1;
|
|
2079
2201
|
}
|
|
2202
|
+
return didRerender;
|
|
2203
|
+
}
|
|
2204
|
+
|
|
2205
|
+
function dedupePendingEffects(queue: PendingEffect[]): void {
|
|
2206
|
+
if (queue.length < 2) {
|
|
2207
|
+
return;
|
|
2208
|
+
}
|
|
2209
|
+
|
|
2210
|
+
const latestBySlot = new Map<Extract<HookSlot, { kind: "effect" }>, PendingEffect>();
|
|
2211
|
+
for (const effect of queue) {
|
|
2212
|
+
latestBySlot.set(effect.slot, effect);
|
|
2213
|
+
}
|
|
2214
|
+
queue.length = 0;
|
|
2215
|
+
queue.push(...latestBySlot.values());
|
|
2080
2216
|
}
|
|
2081
2217
|
|
|
2082
2218
|
function flushEffectFlushRerenders(): void {
|
|
@@ -2299,11 +2435,7 @@ function requireRuntime(): RootRuntime {
|
|
|
2299
2435
|
}
|
|
2300
2436
|
|
|
2301
2437
|
function requireInstance(): ComponentInstance {
|
|
2302
|
-
|
|
2303
|
-
throw new Error("Hooks can only be called while rendering.");
|
|
2304
|
-
}
|
|
2305
|
-
|
|
2306
|
-
return hookRenderState.currentInstance;
|
|
2438
|
+
return hookRenderState.currentInstance ?? materializeInstance();
|
|
2307
2439
|
}
|
|
2308
2440
|
|
|
2309
2441
|
function areHookInputsEqual(
|