@ipxjs/refract 0.3.1 → 0.4.1
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/README.md +54 -16
- package/package.json +11 -3
- package/src/refract/compat/react-dom-client.ts +27 -0
- package/src/refract/compat/react-dom.ts +42 -0
- package/src/refract/compat/react-jsx-dev-runtime.ts +15 -0
- package/src/refract/compat/react-jsx-runtime.ts +32 -0
- package/src/refract/compat/react.ts +238 -0
- package/src/refract/compat/sharedInternals.ts +212 -0
- package/src/refract/coreRenderer.ts +81 -5
- package/src/refract/dom.ts +41 -3
- package/src/refract/features/hooks.ts +129 -5
- package/src/refract/full.ts +20 -1
- package/src/refract/hooks.ts +9 -0
- package/src/refract/hooksRuntime.ts +22 -3
- package/src/refract/portal.ts +18 -0
- package/src/refract/runtimeExtensions.ts +26 -0
- package/tests/compat.test.ts +177 -0
- package/tests/entrypoints.test.ts +18 -0
- package/tests/keyed.test.ts +1 -1
- package/tests/react-compat-shims.d.ts +4 -0
- package/tests/react-router-smoke.test.ts +72 -0
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import {
|
|
2
|
+
useCallback as rawUseCallback,
|
|
3
|
+
useDebugValue as rawUseDebugValue,
|
|
4
|
+
useDeferredValue as rawUseDeferredValue,
|
|
5
|
+
useEffect as rawUseEffect,
|
|
6
|
+
useId as rawUseId,
|
|
7
|
+
useImperativeHandle as rawUseImperativeHandle,
|
|
8
|
+
useInsertionEffect as rawUseInsertionEffect,
|
|
9
|
+
useLayoutEffect as rawUseLayoutEffect,
|
|
10
|
+
useMemo as rawUseMemo,
|
|
11
|
+
useReducer as rawUseReducer,
|
|
12
|
+
useRef as rawUseRef,
|
|
13
|
+
useState as rawUseState,
|
|
14
|
+
useSyncExternalStore as rawUseSyncExternalStore,
|
|
15
|
+
useTransition as rawUseTransition,
|
|
16
|
+
} from "../features/hooks.js";
|
|
17
|
+
import { useContext as rawUseContext } from "../features/context.js";
|
|
18
|
+
import {
|
|
19
|
+
registerAfterComponentRenderHandler,
|
|
20
|
+
registerBeforeComponentRenderHandler,
|
|
21
|
+
} from "../runtimeExtensions.js";
|
|
22
|
+
|
|
23
|
+
const INVALID_HOOK_CALL_MESSAGE = [
|
|
24
|
+
"Invalid hook call. Hooks can only be called inside of the body of a function component.",
|
|
25
|
+
"This could happen for one of the following reasons:",
|
|
26
|
+
"1. You might have mismatching versions of React and the renderer (such as React DOM)",
|
|
27
|
+
"2. You might be breaking the Rules of Hooks",
|
|
28
|
+
"3. You might have more than one copy of React in the same app",
|
|
29
|
+
"See https://react.dev/link/invalid-hook-call for tips about how to debug and fix this problem.",
|
|
30
|
+
].join("\n");
|
|
31
|
+
|
|
32
|
+
export interface ReactCurrentDispatcherCompat {
|
|
33
|
+
current: RefractHookDispatcher | null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface ReactSecretInternalsCompat {
|
|
37
|
+
ReactCurrentDispatcher: ReactCurrentDispatcherCompat;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface ReactClientInternalsCompat {
|
|
41
|
+
H: RefractHookDispatcher | null;
|
|
42
|
+
[key: string]: unknown;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
type ExternalReactLike = {
|
|
46
|
+
__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE?: unknown;
|
|
47
|
+
__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED?: unknown;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export interface RefractHookDispatcher {
|
|
51
|
+
useState: typeof rawUseState;
|
|
52
|
+
useReducer: typeof rawUseReducer;
|
|
53
|
+
useRef: typeof rawUseRef;
|
|
54
|
+
useEffect: typeof rawUseEffect;
|
|
55
|
+
useLayoutEffect: typeof rawUseLayoutEffect;
|
|
56
|
+
useInsertionEffect: typeof rawUseInsertionEffect;
|
|
57
|
+
useMemo: typeof rawUseMemo;
|
|
58
|
+
useCallback: typeof rawUseCallback;
|
|
59
|
+
useContext: typeof rawUseContext;
|
|
60
|
+
useId: typeof rawUseId;
|
|
61
|
+
useSyncExternalStore: typeof rawUseSyncExternalStore;
|
|
62
|
+
useTransition: typeof rawUseTransition;
|
|
63
|
+
useDeferredValue: typeof rawUseDeferredValue;
|
|
64
|
+
useImperativeHandle: typeof rawUseImperativeHandle;
|
|
65
|
+
useDebugValue: typeof rawUseDebugValue;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const dispatcher: RefractHookDispatcher = {
|
|
69
|
+
useState: rawUseState,
|
|
70
|
+
useReducer: rawUseReducer,
|
|
71
|
+
useRef: rawUseRef,
|
|
72
|
+
useEffect: rawUseEffect,
|
|
73
|
+
useLayoutEffect: rawUseLayoutEffect,
|
|
74
|
+
useInsertionEffect: rawUseInsertionEffect,
|
|
75
|
+
useMemo: rawUseMemo,
|
|
76
|
+
useCallback: rawUseCallback,
|
|
77
|
+
useContext: rawUseContext,
|
|
78
|
+
useId: rawUseId,
|
|
79
|
+
useSyncExternalStore: rawUseSyncExternalStore,
|
|
80
|
+
useTransition: rawUseTransition,
|
|
81
|
+
useDeferredValue: rawUseDeferredValue,
|
|
82
|
+
useImperativeHandle: rawUseImperativeHandle,
|
|
83
|
+
useDebugValue: rawUseDebugValue,
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const clientInternals: ReactClientInternalsCompat = {
|
|
87
|
+
H: null,
|
|
88
|
+
A: null,
|
|
89
|
+
T: null,
|
|
90
|
+
S: null,
|
|
91
|
+
actQueue: null,
|
|
92
|
+
asyncTransitions: 0,
|
|
93
|
+
isBatchingLegacy: false,
|
|
94
|
+
didScheduleLegacyUpdate: false,
|
|
95
|
+
didUsePromise: false,
|
|
96
|
+
thrownErrors: [],
|
|
97
|
+
getCurrentStack: null,
|
|
98
|
+
recentlyCreatedOwnerStacks: 0,
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const secretInternals: ReactSecretInternalsCompat = {
|
|
102
|
+
ReactCurrentDispatcher: {
|
|
103
|
+
current: null,
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const externalClientInternals = new Set<ReactClientInternalsCompat>();
|
|
108
|
+
const externalSecretInternals = new Set<ReactSecretInternalsCompat>();
|
|
109
|
+
const dispatcherStack: (RefractHookDispatcher | null)[] = [];
|
|
110
|
+
|
|
111
|
+
let runtimeInitialized = false;
|
|
112
|
+
|
|
113
|
+
export const __CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE = clientInternals;
|
|
114
|
+
export const __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED = secretInternals;
|
|
115
|
+
|
|
116
|
+
export function resolveDispatcher(): RefractHookDispatcher {
|
|
117
|
+
const active = clientInternals.H ?? secretInternals.ReactCurrentDispatcher.current;
|
|
118
|
+
if (active == null) {
|
|
119
|
+
throw new Error(INVALID_HOOK_CALL_MESSAGE);
|
|
120
|
+
}
|
|
121
|
+
return active;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function ensureHookDispatcherRuntime(): void {
|
|
125
|
+
if (runtimeInitialized) return;
|
|
126
|
+
runtimeInitialized = true;
|
|
127
|
+
registerBeforeComponentRenderHandler(beforeComponentRender);
|
|
128
|
+
registerAfterComponentRenderHandler(afterComponentRender);
|
|
129
|
+
tryAutoRegisterExternalReact();
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function registerExternalReactModule(moduleValue: unknown): void {
|
|
133
|
+
if (!moduleValue || typeof moduleValue !== "object") return;
|
|
134
|
+
const moduleRecord = moduleValue as ExternalReactLike;
|
|
135
|
+
|
|
136
|
+
const candidateClient = moduleRecord.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE;
|
|
137
|
+
if (candidateClient && typeof candidateClient === "object" && "H" in (candidateClient as Record<string, unknown>)) {
|
|
138
|
+
externalClientInternals.add(candidateClient as ReactClientInternalsCompat);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const candidateSecret = moduleRecord.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED;
|
|
142
|
+
const dispatcherHolder = candidateSecret as { ReactCurrentDispatcher?: unknown } | undefined;
|
|
143
|
+
if (
|
|
144
|
+
dispatcherHolder
|
|
145
|
+
&& typeof dispatcherHolder === "object"
|
|
146
|
+
&& dispatcherHolder.ReactCurrentDispatcher
|
|
147
|
+
&& typeof dispatcherHolder.ReactCurrentDispatcher === "object"
|
|
148
|
+
&& "current" in (dispatcherHolder.ReactCurrentDispatcher as Record<string, unknown>)
|
|
149
|
+
) {
|
|
150
|
+
externalSecretInternals.add(candidateSecret as ReactSecretInternalsCompat);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
syncDispatcherToExternal();
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function beforeComponentRender(): void {
|
|
157
|
+
dispatcherStack.push(clientInternals.H);
|
|
158
|
+
setDispatcher(dispatcher);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function afterComponentRender(): void {
|
|
162
|
+
const previous = dispatcherStack.pop() ?? null;
|
|
163
|
+
setDispatcher(previous);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function setDispatcher(value: RefractHookDispatcher | null): void {
|
|
167
|
+
clientInternals.H = value;
|
|
168
|
+
secretInternals.ReactCurrentDispatcher.current = value;
|
|
169
|
+
syncDispatcherToExternal();
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function syncDispatcherToExternal(): void {
|
|
173
|
+
for (const ext of externalClientInternals) {
|
|
174
|
+
ext.H = clientInternals.H;
|
|
175
|
+
}
|
|
176
|
+
for (const ext of externalSecretInternals) {
|
|
177
|
+
ext.ReactCurrentDispatcher.current = clientInternals.H;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function tryAutoRegisterExternalReact(): void {
|
|
182
|
+
const maybeRequire = getNodeRequire();
|
|
183
|
+
if (!maybeRequire) return;
|
|
184
|
+
try {
|
|
185
|
+
registerExternalReactModule(maybeRequire("react"));
|
|
186
|
+
} catch {
|
|
187
|
+
// External React module is optional.
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function getNodeRequire(): ((id: string) => unknown) | null {
|
|
192
|
+
const globalRecord = globalThis as Record<string, unknown>;
|
|
193
|
+
const candidateRequire = globalRecord.require;
|
|
194
|
+
if (typeof candidateRequire === "function") {
|
|
195
|
+
return candidateRequire as (id: string) => unknown;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const nodeProcess = globalRecord.process as { mainModule?: { require?: (id: string) => unknown } } | undefined;
|
|
199
|
+
const mainModuleRequire = nodeProcess?.mainModule?.require;
|
|
200
|
+
if (typeof mainModuleRequire === "function") {
|
|
201
|
+
return mainModuleRequire.bind(nodeProcess?.mainModule);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
try {
|
|
205
|
+
const dynamicRequire = Function("return typeof require === 'function' ? require : null")() as
|
|
206
|
+
| ((id: string) => unknown)
|
|
207
|
+
| null;
|
|
208
|
+
return dynamicRequire;
|
|
209
|
+
} catch {
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
@@ -2,9 +2,12 @@ import type { VNode, Fiber, Props } from "./types.js";
|
|
|
2
2
|
import { PLACEMENT, UPDATE } from "./types.js";
|
|
3
3
|
import { reconcileChildren } from "./reconcile.js";
|
|
4
4
|
import { Fragment } from "./createElement.js";
|
|
5
|
+
import { Portal } from "./portal.js";
|
|
5
6
|
import { createDom, applyProps } from "./dom.js";
|
|
6
7
|
import {
|
|
8
|
+
runAfterComponentRenderHandlers,
|
|
7
9
|
runAfterCommitHandlers,
|
|
10
|
+
runBeforeComponentRenderHandlers,
|
|
8
11
|
runCommitHandlers,
|
|
9
12
|
runFiberCleanupHandlers,
|
|
10
13
|
shouldBailoutComponent,
|
|
@@ -52,6 +55,7 @@ export function renderFiber(vnode: VNode, container: Node): void {
|
|
|
52
55
|
function performWork(fiber: Fiber): void {
|
|
53
56
|
const isComponent = typeof fiber.type === "function";
|
|
54
57
|
const isFragment = fiber.type === Fragment;
|
|
58
|
+
const isPortal = fiber.type === Portal;
|
|
55
59
|
|
|
56
60
|
if (isComponent) {
|
|
57
61
|
if (fiber.alternate && fiber.flags === UPDATE && shouldBailoutComponent(fiber)) {
|
|
@@ -62,22 +66,32 @@ function performWork(fiber: Fiber): void {
|
|
|
62
66
|
fiber._hookIndex = 0;
|
|
63
67
|
if (!fiber.hooks) fiber.hooks = [];
|
|
64
68
|
|
|
65
|
-
const comp = fiber.type as (props: Props) =>
|
|
69
|
+
const comp = fiber.type as (props: Props) => unknown;
|
|
70
|
+
runBeforeComponentRenderHandlers(fiber);
|
|
66
71
|
try {
|
|
67
|
-
const children =
|
|
72
|
+
const children = normalizeRenderedChildren(comp(fiber.props));
|
|
68
73
|
reconcileChildren(fiber, children);
|
|
69
74
|
} catch (error) {
|
|
70
75
|
if (!tryHandleRenderError(fiber, error)) throw error;
|
|
76
|
+
} finally {
|
|
77
|
+
runAfterComponentRenderHandlers(fiber);
|
|
71
78
|
}
|
|
72
79
|
} else if (isFragment) {
|
|
73
|
-
reconcileChildren(fiber, fiber.props.children
|
|
80
|
+
reconcileChildren(fiber, normalizeChildrenProp(fiber.props.children));
|
|
81
|
+
} else if (isPortal) {
|
|
82
|
+
const container = fiber.props.container;
|
|
83
|
+
if (!(container instanceof Node)) {
|
|
84
|
+
throw new TypeError("createPortal expects a valid DOM Node container");
|
|
85
|
+
}
|
|
86
|
+
fiber.dom = container;
|
|
87
|
+
reconcileChildren(fiber, normalizeChildrenProp(fiber.props.children));
|
|
74
88
|
} else {
|
|
75
89
|
if (!fiber.dom) {
|
|
76
90
|
fiber.dom = createDom(fiber);
|
|
77
91
|
}
|
|
78
92
|
// Skip children when dangerouslySetInnerHTML is used
|
|
79
93
|
if (!fiber.props.dangerouslySetInnerHTML) {
|
|
80
|
-
reconcileChildren(fiber, fiber.props.children
|
|
94
|
+
reconcileChildren(fiber, normalizeChildrenProp(fiber.props.children));
|
|
81
95
|
}
|
|
82
96
|
}
|
|
83
97
|
|
|
@@ -90,6 +104,41 @@ function performWork(fiber: Fiber): void {
|
|
|
90
104
|
advanceWork(fiber);
|
|
91
105
|
}
|
|
92
106
|
|
|
107
|
+
type RenderedChild = VNode | string | number | boolean | null | undefined | RenderedChild[];
|
|
108
|
+
|
|
109
|
+
function normalizeRenderedChildren(rendered: unknown): VNode[] {
|
|
110
|
+
return flattenRenderedChildren([rendered as RenderedChild]);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function normalizeChildrenProp(children: unknown): VNode[] {
|
|
114
|
+
if (children === undefined) return [];
|
|
115
|
+
if (Array.isArray(children)) {
|
|
116
|
+
return flattenRenderedChildren(children as RenderedChild[]);
|
|
117
|
+
}
|
|
118
|
+
return flattenRenderedChildren([children as RenderedChild]);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function flattenRenderedChildren(raw: RenderedChild[]): VNode[] {
|
|
122
|
+
const result: VNode[] = [];
|
|
123
|
+
for (const child of raw) {
|
|
124
|
+
if (child == null || typeof child === "boolean") continue;
|
|
125
|
+
if (Array.isArray(child)) {
|
|
126
|
+
result.push(...flattenRenderedChildren(child));
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
if (typeof child === "string" || typeof child === "number") {
|
|
130
|
+
result.push({ type: "TEXT", props: { nodeValue: String(child) }, key: null });
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
result.push(child);
|
|
134
|
+
}
|
|
135
|
+
return result;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function isPortalFiber(fiber: Fiber): boolean {
|
|
139
|
+
return fiber.type === Portal;
|
|
140
|
+
}
|
|
141
|
+
|
|
93
142
|
function advanceWork(fiber: Fiber): void {
|
|
94
143
|
let next: Fiber | null = fiber;
|
|
95
144
|
while (next) {
|
|
@@ -105,6 +154,10 @@ function advanceWork(fiber: Fiber): void {
|
|
|
105
154
|
function getNextDomSibling(fiber: Fiber): Node | null {
|
|
106
155
|
let sib: Fiber | null = fiber.sibling;
|
|
107
156
|
while (sib) {
|
|
157
|
+
if (isPortalFiber(sib)) {
|
|
158
|
+
sib = sib.sibling;
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
108
161
|
// Skip any sibling that is itself being placed/moved
|
|
109
162
|
if (sib.flags & PLACEMENT) {
|
|
110
163
|
sib = sib.sibling;
|
|
@@ -125,6 +178,10 @@ function collectChildDomNodes(fiber: Fiber): Node[] {
|
|
|
125
178
|
const nodes: Node[] = [];
|
|
126
179
|
function walk(f: Fiber | null): void {
|
|
127
180
|
while (f) {
|
|
181
|
+
if (isPortalFiber(f)) {
|
|
182
|
+
f = f.sibling;
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
128
185
|
if (f.dom) {
|
|
129
186
|
nodes.push(f.dom);
|
|
130
187
|
} else {
|
|
@@ -139,6 +196,7 @@ function collectChildDomNodes(fiber: Fiber): Node[] {
|
|
|
139
196
|
|
|
140
197
|
/** Get the first committed DOM node in a fiber subtree */
|
|
141
198
|
function getFirstCommittedDom(fiber: Fiber): Node | null {
|
|
199
|
+
if (isPortalFiber(fiber)) return null;
|
|
142
200
|
if (fiber.dom && !(fiber.flags & PLACEMENT)) return fiber.dom;
|
|
143
201
|
let child = fiber.child;
|
|
144
202
|
while (child) {
|
|
@@ -160,6 +218,13 @@ function commitRoot(rootFiber: Fiber): void {
|
|
|
160
218
|
}
|
|
161
219
|
|
|
162
220
|
function commitWork(fiber: Fiber): void {
|
|
221
|
+
if (isPortalFiber(fiber)) {
|
|
222
|
+
fiber.flags = 0;
|
|
223
|
+
if (fiber.child) commitWork(fiber.child);
|
|
224
|
+
if (fiber.sibling) commitWork(fiber.sibling);
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
|
|
163
228
|
let parentFiber = fiber.parent;
|
|
164
229
|
while (parentFiber && !parentFiber.dom) {
|
|
165
230
|
parentFiber = parentFiber.parent;
|
|
@@ -226,7 +291,13 @@ function commitDeletion(fiber: Fiber): void {
|
|
|
226
291
|
if (fiber.dom && fiber.props.ref) {
|
|
227
292
|
setRef(fiber.props.ref, null);
|
|
228
293
|
}
|
|
229
|
-
if (fiber
|
|
294
|
+
if (isPortalFiber(fiber)) {
|
|
295
|
+
let child: Fiber | null = fiber.child;
|
|
296
|
+
while (child) {
|
|
297
|
+
commitDeletion(child);
|
|
298
|
+
child = child.sibling;
|
|
299
|
+
}
|
|
300
|
+
} else if (fiber.dom) {
|
|
230
301
|
fiber.dom.parentNode?.removeChild(fiber.dom);
|
|
231
302
|
} else if (fiber.child) {
|
|
232
303
|
// Fragment/component — delete children
|
|
@@ -289,3 +360,8 @@ function flushRenders(): void {
|
|
|
289
360
|
}
|
|
290
361
|
pendingContainers.clear();
|
|
291
362
|
}
|
|
363
|
+
|
|
364
|
+
export function flushPendingRenders(): void {
|
|
365
|
+
if (!flushScheduled) return;
|
|
366
|
+
flushRenders();
|
|
367
|
+
}
|
package/src/refract/dom.ts
CHANGED
|
@@ -13,6 +13,8 @@ export type UnsafeUrlPropChecker = (key: string, value: unknown) => boolean;
|
|
|
13
13
|
|
|
14
14
|
let htmlSanitizer: HtmlSanitizer = identitySanitizer;
|
|
15
15
|
let unsafeUrlPropChecker: UnsafeUrlPropChecker = () => false;
|
|
16
|
+
let reactCompatEventMode = false;
|
|
17
|
+
const reactCompatWrappers = new WeakMap<EventListener, EventListener>();
|
|
16
18
|
|
|
17
19
|
export function setHtmlSanitizer(sanitizer: HtmlSanitizer | null): void {
|
|
18
20
|
htmlSanitizer = sanitizer ?? identitySanitizer;
|
|
@@ -22,10 +24,46 @@ export function setUnsafeUrlPropChecker(checker: UnsafeUrlPropChecker | null): v
|
|
|
22
24
|
unsafeUrlPropChecker = checker ?? (() => false);
|
|
23
25
|
}
|
|
24
26
|
|
|
27
|
+
export function setReactCompatEventMode(enabled: boolean): void {
|
|
28
|
+
reactCompatEventMode = enabled;
|
|
29
|
+
}
|
|
30
|
+
|
|
25
31
|
function identitySanitizer(html: string): string {
|
|
26
32
|
return html;
|
|
27
33
|
}
|
|
28
34
|
|
|
35
|
+
function getEventListener(handler: unknown): EventListener {
|
|
36
|
+
if (typeof handler !== "function") {
|
|
37
|
+
return handler as EventListener;
|
|
38
|
+
}
|
|
39
|
+
if (!reactCompatEventMode) {
|
|
40
|
+
return handler as EventListener;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const typedHandler = handler as EventListener;
|
|
44
|
+
const existing = reactCompatWrappers.get(typedHandler);
|
|
45
|
+
if (existing) return existing;
|
|
46
|
+
|
|
47
|
+
const wrapped: EventListener = (event: Event) => {
|
|
48
|
+
const eventRecord = event as unknown as Record<string, unknown>;
|
|
49
|
+
if (!("nativeEvent" in eventRecord)) {
|
|
50
|
+
try {
|
|
51
|
+
Object.defineProperty(event, "nativeEvent", {
|
|
52
|
+
configurable: true,
|
|
53
|
+
enumerable: false,
|
|
54
|
+
value: event,
|
|
55
|
+
writable: false,
|
|
56
|
+
});
|
|
57
|
+
} catch {
|
|
58
|
+
// ignore if event object is non-extensible
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
typedHandler(event);
|
|
62
|
+
};
|
|
63
|
+
reactCompatWrappers.set(typedHandler, wrapped);
|
|
64
|
+
return wrapped;
|
|
65
|
+
}
|
|
66
|
+
|
|
29
67
|
/** Create a real DOM node from a fiber */
|
|
30
68
|
export function createDom(fiber: Fiber): Node {
|
|
31
69
|
if (fiber.type === "TEXT") {
|
|
@@ -61,7 +99,7 @@ export function applyProps(
|
|
|
61
99
|
if (key === "children" || key === "key" || key === "ref") continue;
|
|
62
100
|
if (!(key in newProps)) {
|
|
63
101
|
if (key.startsWith("on")) {
|
|
64
|
-
el.removeEventListener(key.slice(2).toLowerCase(), oldProps[key]
|
|
102
|
+
el.removeEventListener(key.slice(2).toLowerCase(), getEventListener(oldProps[key]));
|
|
65
103
|
} else {
|
|
66
104
|
el.removeAttribute(key);
|
|
67
105
|
}
|
|
@@ -106,9 +144,9 @@ export function applyProps(
|
|
|
106
144
|
if (key.startsWith("on")) {
|
|
107
145
|
const event = key.slice(2).toLowerCase();
|
|
108
146
|
if (oldProps[key]) {
|
|
109
|
-
el.removeEventListener(event, oldProps[key]
|
|
147
|
+
el.removeEventListener(event, getEventListener(oldProps[key]));
|
|
110
148
|
}
|
|
111
|
-
el.addEventListener(event, newProps[key]
|
|
149
|
+
el.addEventListener(event, getEventListener(newProps[key]));
|
|
112
150
|
} else {
|
|
113
151
|
if (unsafeUrlPropChecker(key, newProps[key])) {
|
|
114
152
|
el.removeAttribute(key);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Hook } from "../types.js";
|
|
2
2
|
import { currentFiber, scheduleRender } from "../coreRenderer.js";
|
|
3
|
-
import { markPendingEffects } from "../hooksRuntime.js";
|
|
3
|
+
import { markPendingEffects, markPendingInsertionEffects, markPendingLayoutEffects } from "../hooksRuntime.js";
|
|
4
4
|
|
|
5
5
|
function getHook(): Hook {
|
|
6
6
|
const fiber = currentFiber!;
|
|
@@ -15,13 +15,15 @@ function getHook(): Hook {
|
|
|
15
15
|
return hook;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
export function useState<T>(initial: T): [T, (value: T | ((prev: T) => T)) => void] {
|
|
18
|
+
export function useState<T>(initial: T | (() => T)): [T, (value: T | ((prev: T) => T)) => void] {
|
|
19
19
|
const hook = getHook();
|
|
20
20
|
const fiber = currentFiber!;
|
|
21
21
|
|
|
22
22
|
// Initialize on first render
|
|
23
23
|
if (hook.queue === undefined) {
|
|
24
|
-
hook.state = initial
|
|
24
|
+
hook.state = typeof initial === "function"
|
|
25
|
+
? (initial as () => T)()
|
|
26
|
+
: initial;
|
|
25
27
|
hook.queue = [];
|
|
26
28
|
}
|
|
27
29
|
|
|
@@ -72,6 +74,44 @@ export function useEffect(effect: () => EffectCleanup, deps?: unknown[]): void {
|
|
|
72
74
|
}
|
|
73
75
|
}
|
|
74
76
|
|
|
77
|
+
export function useLayoutEffect(effect: () => EffectCleanup, deps?: unknown[]): void {
|
|
78
|
+
const hook = getHook() as EffectHook;
|
|
79
|
+
const fiber = currentFiber!;
|
|
80
|
+
|
|
81
|
+
if (hook.state === undefined) {
|
|
82
|
+
hook.state = { effect, deps, cleanup: undefined, pending: true };
|
|
83
|
+
markPendingLayoutEffects(fiber);
|
|
84
|
+
} else {
|
|
85
|
+
if (depsChanged(hook.state.deps, deps)) {
|
|
86
|
+
hook.state.effect = effect;
|
|
87
|
+
hook.state.deps = deps;
|
|
88
|
+
hook.state.pending = true;
|
|
89
|
+
markPendingLayoutEffects(fiber);
|
|
90
|
+
} else {
|
|
91
|
+
hook.state.pending = false;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function useInsertionEffect(effect: () => EffectCleanup, deps?: unknown[]): void {
|
|
97
|
+
const hook = getHook() as EffectHook;
|
|
98
|
+
const fiber = currentFiber!;
|
|
99
|
+
|
|
100
|
+
if (hook.state === undefined) {
|
|
101
|
+
hook.state = { effect, deps, cleanup: undefined, pending: true };
|
|
102
|
+
markPendingInsertionEffects(fiber);
|
|
103
|
+
} else {
|
|
104
|
+
if (depsChanged(hook.state.deps, deps)) {
|
|
105
|
+
hook.state.effect = effect;
|
|
106
|
+
hook.state.deps = deps;
|
|
107
|
+
hook.state.pending = true;
|
|
108
|
+
markPendingInsertionEffects(fiber);
|
|
109
|
+
} else {
|
|
110
|
+
hook.state.pending = false;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
75
115
|
interface RefHook extends Hook {
|
|
76
116
|
state: { current: unknown };
|
|
77
117
|
}
|
|
@@ -108,9 +148,25 @@ export function useCallback<T extends Function>(cb: T, deps: unknown[]): T {
|
|
|
108
148
|
|
|
109
149
|
export function useReducer<S, A>(
|
|
110
150
|
reducer: (state: S, action: A) => S,
|
|
111
|
-
|
|
151
|
+
initialArg: S,
|
|
152
|
+
): [S, (action: A) => void];
|
|
153
|
+
|
|
154
|
+
export function useReducer<S, A, I>(
|
|
155
|
+
reducer: (state: S, action: A) => S,
|
|
156
|
+
initialArg: I,
|
|
157
|
+
init: (arg: I) => S,
|
|
158
|
+
): [S, (action: A) => void];
|
|
159
|
+
|
|
160
|
+
export function useReducer<S, A, I>(
|
|
161
|
+
reducer: (state: S, action: A) => S,
|
|
162
|
+
initialArg: S | I,
|
|
163
|
+
init?: (arg: I) => S,
|
|
112
164
|
): [S, (action: A) => void] {
|
|
113
|
-
const [state, setState] = useState(
|
|
165
|
+
const [state, setState] = useState<S>(() => (
|
|
166
|
+
init
|
|
167
|
+
? init(initialArg as I)
|
|
168
|
+
: initialArg as S
|
|
169
|
+
));
|
|
114
170
|
const dispatch = (action: A) => {
|
|
115
171
|
setState((prev) => reducer(prev, action));
|
|
116
172
|
};
|
|
@@ -121,6 +177,74 @@ export function createRef<T = unknown>(): { current: T | null } {
|
|
|
121
177
|
return { current: null };
|
|
122
178
|
}
|
|
123
179
|
|
|
180
|
+
let idCounter = 0;
|
|
181
|
+
|
|
182
|
+
export function useId(): string {
|
|
183
|
+
const hook = getHook();
|
|
184
|
+
if (hook.state === undefined) {
|
|
185
|
+
hook.state = `:r${idCounter++}:`;
|
|
186
|
+
}
|
|
187
|
+
return hook.state as string;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function useImperativeHandle<T>(
|
|
191
|
+
ref: { current: T | null } | ((value: T | null) => void) | null | undefined,
|
|
192
|
+
create: () => T,
|
|
193
|
+
deps?: unknown[],
|
|
194
|
+
): void {
|
|
195
|
+
useLayoutEffect(() => {
|
|
196
|
+
const value = create();
|
|
197
|
+
if (typeof ref === "function") {
|
|
198
|
+
ref(value);
|
|
199
|
+
return () => ref(null);
|
|
200
|
+
}
|
|
201
|
+
if (ref && typeof ref === "object" && "current" in ref) {
|
|
202
|
+
ref.current = value;
|
|
203
|
+
return () => {
|
|
204
|
+
ref.current = null;
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
return;
|
|
208
|
+
}, deps);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export function useDebugValue(_value: unknown): void {
|
|
212
|
+
// no-op compatibility hook
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export function startTransition(callback: () => void): void {
|
|
216
|
+
callback();
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export function useTransition(): [boolean, (callback: () => void) => void] {
|
|
220
|
+
return [false, startTransition];
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export function useDeferredValue<T>(value: T, _initialValue?: T): T {
|
|
224
|
+
return value;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export function useSyncExternalStore<T>(
|
|
228
|
+
subscribe: (onStoreChange: () => void) => () => void,
|
|
229
|
+
getSnapshot: () => T,
|
|
230
|
+
_getServerSnapshot?: () => T,
|
|
231
|
+
): T {
|
|
232
|
+
const [snapshot, setSnapshot] = useState<T>(getSnapshot());
|
|
233
|
+
|
|
234
|
+
useEffect(() => {
|
|
235
|
+
const handleStoreChange = () => {
|
|
236
|
+
setSnapshot(getSnapshot());
|
|
237
|
+
};
|
|
238
|
+
const unsubscribe = subscribe(handleStoreChange);
|
|
239
|
+
handleStoreChange();
|
|
240
|
+
return () => {
|
|
241
|
+
unsubscribe();
|
|
242
|
+
};
|
|
243
|
+
}, [subscribe, getSnapshot]);
|
|
244
|
+
|
|
245
|
+
return snapshot;
|
|
246
|
+
}
|
|
247
|
+
|
|
124
248
|
export function useErrorBoundary(): [
|
|
125
249
|
unknown,
|
|
126
250
|
() => void,
|
package/src/refract/full.ts
CHANGED
|
@@ -3,8 +3,27 @@ export { render } from "./render.js";
|
|
|
3
3
|
export { memo } from "./memo.js";
|
|
4
4
|
export { setHtmlSanitizer } from "./features/security.js";
|
|
5
5
|
export { setDevtoolsHook, DEVTOOLS_GLOBAL_HOOK } from "./devtools.js";
|
|
6
|
-
export {
|
|
6
|
+
export {
|
|
7
|
+
useState,
|
|
8
|
+
useEffect,
|
|
9
|
+
useLayoutEffect,
|
|
10
|
+
useInsertionEffect,
|
|
11
|
+
useRef,
|
|
12
|
+
useMemo,
|
|
13
|
+
useCallback,
|
|
14
|
+
useReducer,
|
|
15
|
+
useSyncExternalStore,
|
|
16
|
+
useImperativeHandle,
|
|
17
|
+
useDebugValue,
|
|
18
|
+
useId,
|
|
19
|
+
useTransition,
|
|
20
|
+
startTransition,
|
|
21
|
+
useDeferredValue,
|
|
22
|
+
createRef,
|
|
23
|
+
useErrorBoundary,
|
|
24
|
+
} from "./features/hooks.js";
|
|
7
25
|
export { createContext, useContext } from "./features/context.js";
|
|
26
|
+
export { createPortal } from "./portal.js";
|
|
8
27
|
export type { VNode, Props, Component } from "./types.js";
|
|
9
28
|
export type {
|
|
10
29
|
RefractDevtoolsHook,
|
package/src/refract/hooks.ts
CHANGED
|
@@ -1,10 +1,19 @@
|
|
|
1
1
|
export {
|
|
2
2
|
useState,
|
|
3
3
|
useEffect,
|
|
4
|
+
useLayoutEffect,
|
|
5
|
+
useInsertionEffect,
|
|
4
6
|
useRef,
|
|
5
7
|
useMemo,
|
|
6
8
|
useCallback,
|
|
7
9
|
useReducer,
|
|
10
|
+
useSyncExternalStore,
|
|
11
|
+
useImperativeHandle,
|
|
12
|
+
useDebugValue,
|
|
13
|
+
useId,
|
|
14
|
+
useTransition,
|
|
15
|
+
startTransition,
|
|
16
|
+
useDeferredValue,
|
|
8
17
|
createRef,
|
|
9
18
|
useErrorBoundary,
|
|
10
19
|
depsChanged,
|