@ipxjs/refract 0.3.1 → 0.4.0

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.
@@ -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) => VNode;
69
+ const comp = fiber.type as (props: Props) => unknown;
70
+ runBeforeComponentRenderHandlers(fiber);
66
71
  try {
67
- const children = [comp(fiber.props)];
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.dom) {
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
+ }
@@ -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] as EventListener);
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] as EventListener);
147
+ el.removeEventListener(event, getEventListener(oldProps[key]));
110
148
  }
111
- el.addEventListener(event, newProps[key] as EventListener);
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
- initialState: S,
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(initialState);
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,
@@ -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 { useState, useEffect, useRef, useMemo, useCallback, useReducer, createRef, useErrorBoundary } from "./features/hooks.js";
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,
@@ -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,