what-react 0.10.0 → 0.11.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.
- package/README.md +12 -11
- package/package.json +7 -3
- package/src/dom.js +52 -29
- package/src/hooks.js +351 -0
- package/src/index.js +264 -386
- package/src/runtime.js +1147 -0
- package/src/vite-plugin.js +64 -3
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# what-react
|
|
2
2
|
|
|
3
|
-
React compatibility layer for [What Framework](https://whatfw.com).
|
|
3
|
+
React compatibility layer for [What Framework](https://whatfw.com). Run React ecosystem libraries on What — `what-react` ships a dedicated React-semantics runtime (value-returning hooks, re-renders with keyed reconciliation, working context) that coexists with What's run-once signal engine.
|
|
4
4
|
|
|
5
|
-
**
|
|
5
|
+
**Verified end-to-end (2026-06-09, real browser + jsdom CI):** zustand, @tanstack/react-query, react-hook-form, react-hot-toast, @headlessui/react, framer-motion. Other libraries are untested on the current runtime — see [REACT-COMPAT.md](https://github.com/CelsianJs/what-framework/blob/main/REACT-COMPAT.md) for the full verified matrix, method, and known limitations.
|
|
6
6
|
|
|
7
7
|
## Install
|
|
8
8
|
|
|
@@ -75,22 +75,23 @@ function Todos() {
|
|
|
75
75
|
|
|
76
76
|
## How It Works
|
|
77
77
|
|
|
78
|
-
`what-react`
|
|
78
|
+
`what-react` ships its own React-semantics runtime (`src/runtime.js`) — what-core is not modified:
|
|
79
79
|
|
|
80
|
-
- `useState
|
|
81
|
-
-
|
|
82
|
-
-
|
|
83
|
-
- `
|
|
84
|
-
- `
|
|
85
|
-
- `
|
|
80
|
+
- Hooks return **values** (`useState` → `[value, setState]`, `useMemo` → the value) with React's deps/cleanup semantics
|
|
81
|
+
- Components **re-render** on state change; output is reconciled with a keyed, type-matched diff, so DOM elements and child component state are preserved
|
|
82
|
+
- `createContext` / `useContext` propagate real values through the component tree (nested providers, memo-bailout propagation)
|
|
83
|
+
- Element refs attach after DOM insertion; `useLayoutEffect` is synchronous at commit; `useEffect` is async; children's effects run before the parent's
|
|
84
|
+
- Class components (`Component`, `PureComponent`) are wrapped as function components (state, lifecycle, error boundaries, `contextType`)
|
|
85
|
+
- `createPortal`, `lazy` + minimal `Suspense`, `React.memo` with real skip semantics
|
|
86
|
+
- Compat semantics apply ONLY to elements created via what-react's `createElement`/JSX runtime — native What components keep their run-once signal semantics, in both directions (What-inside-React and React-inside-What)
|
|
86
87
|
|
|
87
|
-
|
|
88
|
+
React libraries import `react` and call its hooks. By aliasing `react` to `what-react`, those hooks execute on this runtime. SSR of compat components is not supported (browser/jsdom only) — see REACT-COMPAT.md for the full limitations list.
|
|
88
89
|
|
|
89
90
|
## What's Implemented
|
|
90
91
|
|
|
91
92
|
### React (index.js)
|
|
92
93
|
|
|
93
|
-
`useState`, `useEffect`, `useLayoutEffect`, `useInsertionEffect`, `useMemo`, `useCallback`, `useRef`, `useContext`, `useReducer`, `useImperativeHandle`, `useId`, `useDebugValue`, `useSyncExternalStore`, `useTransition`, `useDeferredValue`, `createElement`, `createContext`, `createRef`, `createFactory`, `forwardRef`, `cloneElement`, `isValidElement`, `Component`, `PureComponent`, `Fragment`, `Suspense`, `StrictMode`, `memo`, `lazy`, `Children`, `startTransition`
|
|
94
|
+
`useState`, `useEffect`, `useLayoutEffect`, `useInsertionEffect`, `useMemo`, `useCallback`, `useRef`, `useContext`, `useReducer`, `useImperativeHandle`, `useId`, `useDebugValue`, `useSyncExternalStore`, `useTransition`, `useDeferredValue`, `use`, `createElement`, `createContext`, `createRef`, `createFactory`, `forwardRef`, `cloneElement`, `isValidElement`, `Component`, `PureComponent`, `Fragment`, `Suspense`, `StrictMode`, `memo`, `lazy`, `act`, `Children`, `startTransition`
|
|
94
95
|
|
|
95
96
|
### ReactDOM (dom.js)
|
|
96
97
|
|
package/package.json
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "what-react",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "React compatibility layer for What Framework —
|
|
3
|
+
"version": "0.11.0",
|
|
4
|
+
"description": "React compatibility layer for What Framework — real React semantics (value hooks, re-renders, context) on a dedicated compat runtime",
|
|
5
5
|
"type": "module",
|
|
6
|
+
"sideEffects": false,
|
|
6
7
|
"main": "src/index.js",
|
|
8
|
+
"scripts": {
|
|
9
|
+
"test": "node --test 'test/*.test.js'"
|
|
10
|
+
},
|
|
7
11
|
"exports": {
|
|
8
12
|
".": "./src/index.js",
|
|
9
13
|
"./dom": "./src/dom.js",
|
|
@@ -23,7 +27,7 @@
|
|
|
23
27
|
"compatibility"
|
|
24
28
|
],
|
|
25
29
|
"peerDependencies": {
|
|
26
|
-
"what-core": "^0.
|
|
30
|
+
"what-core": "^0.11.0"
|
|
27
31
|
},
|
|
28
32
|
"author": "ZVN DEV (https://zvndev.com)",
|
|
29
33
|
"license": "MIT",
|
package/src/dom.js
CHANGED
|
@@ -1,34 +1,45 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* what-react/dom — ReactDOM compatibility layer
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Renders through what-react's compat runtime (React semantics: re-renders,
|
|
5
|
+
* keyed reconciliation, value hooks). Native What vnodes inside the tree are
|
|
6
|
+
* delegated to what-core automatically by the runtime.
|
|
5
7
|
*/
|
|
6
8
|
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
+
import {
|
|
10
|
+
mountRoot,
|
|
11
|
+
patchRoot,
|
|
12
|
+
unmountRoot,
|
|
13
|
+
flushUpdates,
|
|
14
|
+
_flushPassive,
|
|
15
|
+
} from './runtime.js';
|
|
9
16
|
|
|
10
17
|
// ---- createRoot (React 18) ----
|
|
11
18
|
|
|
12
19
|
export function createRoot(container) {
|
|
13
|
-
let
|
|
20
|
+
let root = null;
|
|
14
21
|
|
|
15
22
|
return {
|
|
16
23
|
render(element) {
|
|
17
|
-
if (
|
|
18
|
-
|
|
24
|
+
if (root) {
|
|
25
|
+
patchRoot(root, element);
|
|
26
|
+
} else {
|
|
27
|
+
container.textContent = '';
|
|
28
|
+
root = mountRoot(element, container);
|
|
29
|
+
}
|
|
19
30
|
},
|
|
20
31
|
unmount() {
|
|
21
|
-
if (
|
|
22
|
-
|
|
23
|
-
|
|
32
|
+
if (root) {
|
|
33
|
+
unmountRoot(root);
|
|
34
|
+
root = null;
|
|
24
35
|
}
|
|
25
|
-
container.
|
|
36
|
+
container.textContent = '';
|
|
26
37
|
},
|
|
27
38
|
};
|
|
28
39
|
}
|
|
29
40
|
|
|
30
41
|
// ---- hydrateRoot ----
|
|
31
|
-
//
|
|
42
|
+
// No true hydration — replaces server-rendered content with a fresh mount.
|
|
32
43
|
|
|
33
44
|
export function hydrateRoot(container, initialChildren) {
|
|
34
45
|
const root = createRoot(container);
|
|
@@ -38,8 +49,14 @@ export function hydrateRoot(container, initialChildren) {
|
|
|
38
49
|
|
|
39
50
|
// ---- render (React 17 legacy) ----
|
|
40
51
|
|
|
52
|
+
const legacyRoots = new WeakMap();
|
|
53
|
+
|
|
41
54
|
export function render(element, container, callback) {
|
|
42
|
-
|
|
55
|
+
let root = legacyRoots.get(container);
|
|
56
|
+
if (!root) {
|
|
57
|
+
root = createRoot(container);
|
|
58
|
+
legacyRoots.set(container, root);
|
|
59
|
+
}
|
|
43
60
|
root.render(element);
|
|
44
61
|
if (callback) queueMicrotask(callback);
|
|
45
62
|
return root;
|
|
@@ -48,53 +65,59 @@ export function render(element, container, callback) {
|
|
|
48
65
|
// ---- unmountComponentAtNode (React 17 legacy) ----
|
|
49
66
|
|
|
50
67
|
export function unmountComponentAtNode(container) {
|
|
68
|
+
const root = legacyRoots.get(container);
|
|
69
|
+
if (root) {
|
|
70
|
+
root.unmount();
|
|
71
|
+
legacyRoots.delete(container);
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
51
74
|
container.innerHTML = '';
|
|
52
75
|
return true;
|
|
53
76
|
}
|
|
54
77
|
|
|
55
78
|
// ---- createPortal ----
|
|
79
|
+
// The compat runtime recognizes '__portal' vnodes and renders children into
|
|
80
|
+
// the target container while keeping context/ownership from the React tree.
|
|
56
81
|
|
|
57
82
|
export function createPortal(children, container, key) {
|
|
58
|
-
|
|
59
|
-
// Core's createDOM routes '__portal' tagged vnodes to the internal portal handler,
|
|
60
|
-
// which renders children into the target container and returns a placeholder comment.
|
|
61
|
-
const portal = {
|
|
83
|
+
return {
|
|
62
84
|
tag: '__portal',
|
|
85
|
+
type: '__portal',
|
|
63
86
|
props: { container, key },
|
|
64
87
|
children: Array.isArray(children) ? children : [children],
|
|
65
|
-
key: key
|
|
88
|
+
key: key ?? null,
|
|
66
89
|
_vnode: true,
|
|
90
|
+
_compat: true,
|
|
67
91
|
};
|
|
68
|
-
|
|
69
|
-
return portal;
|
|
70
92
|
}
|
|
71
93
|
|
|
72
94
|
// ---- flushSync ----
|
|
73
95
|
|
|
74
96
|
export function flushSync(fn) {
|
|
75
|
-
|
|
76
|
-
|
|
97
|
+
let result;
|
|
98
|
+
if (fn) result = fn();
|
|
99
|
+
flushUpdates();
|
|
100
|
+
_flushPassive();
|
|
101
|
+
flushUpdates();
|
|
102
|
+
return result;
|
|
77
103
|
}
|
|
78
104
|
|
|
79
105
|
// ---- findDOMNode (deprecated but needed for legacy packages) ----
|
|
80
106
|
|
|
81
107
|
export function findDOMNode(component) {
|
|
82
108
|
if (component == null) return null;
|
|
83
|
-
|
|
84
|
-
if (component instanceof HTMLElement) return component;
|
|
85
|
-
// Class component instance — look for _domNode or _container
|
|
109
|
+
if (typeof Element !== 'undefined' && component instanceof Element) return component;
|
|
86
110
|
if (component._domNode) return component._domNode;
|
|
87
|
-
|
|
88
|
-
if (component._ref?.current instanceof HTMLElement) return component._ref.current;
|
|
89
|
-
// what-c wrapper element — return the first child
|
|
90
|
-
if (component instanceof Element) return component;
|
|
111
|
+
if (component._ref && component._ref.current instanceof Element) return component._ref.current;
|
|
91
112
|
return null;
|
|
92
113
|
}
|
|
93
114
|
|
|
94
115
|
// ---- batching ----
|
|
95
116
|
|
|
96
117
|
export function unstable_batchedUpdates(fn) {
|
|
97
|
-
fn();
|
|
118
|
+
const result = fn();
|
|
119
|
+
flushUpdates();
|
|
120
|
+
return result;
|
|
98
121
|
}
|
|
99
122
|
|
|
100
123
|
// ---- Version ----
|
package/src/hooks.js
ADDED
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* what-react/hooks — React hooks with REAL React semantics (value-returning).
|
|
3
|
+
*
|
|
4
|
+
* Unlike what-core's hooks (which return signal accessors for the run-once
|
|
5
|
+
* model), these hooks return plain VALUES and trigger re-renders through the
|
|
6
|
+
* compat runtime's scheduler:
|
|
7
|
+
*
|
|
8
|
+
* const [count, setCount] = useState(0); // count is a NUMBER
|
|
9
|
+
* const doubled = useMemo(() => count * 2, [count]); // doubled is a NUMBER
|
|
10
|
+
*
|
|
11
|
+
* They are only valid inside components rendered by the compat runtime
|
|
12
|
+
* (anything created via what-react's createElement / jsx-runtime).
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import {
|
|
16
|
+
_requireInstance,
|
|
17
|
+
_getCurrentInstance,
|
|
18
|
+
_getHookSlot,
|
|
19
|
+
scheduleUpdate,
|
|
20
|
+
_pushLayout,
|
|
21
|
+
_pushPassive,
|
|
22
|
+
flushUpdates,
|
|
23
|
+
} from './runtime.js';
|
|
24
|
+
|
|
25
|
+
function depsChanged(oldDeps, newDeps) {
|
|
26
|
+
if (oldDeps === undefined || newDeps === undefined) return true;
|
|
27
|
+
if (oldDeps === null || newDeps === null) return true;
|
|
28
|
+
if (oldDeps.length !== newDeps.length) return true;
|
|
29
|
+
for (let i = 0; i < oldDeps.length; i++) {
|
|
30
|
+
if (!Object.is(oldDeps[i], newDeps[i])) return true;
|
|
31
|
+
}
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ---- useState ----
|
|
36
|
+
|
|
37
|
+
export function useState(initial) {
|
|
38
|
+
const inst = _requireInstance('useState');
|
|
39
|
+
const slot = _getHookSlot(inst);
|
|
40
|
+
if (!slot.init) {
|
|
41
|
+
slot.init = true;
|
|
42
|
+
slot.value = typeof initial === 'function' ? initial() : initial;
|
|
43
|
+
slot.set = (next) => {
|
|
44
|
+
const resolved = typeof next === 'function' ? next(slot.value) : next;
|
|
45
|
+
if (Object.is(resolved, slot.value)) return;
|
|
46
|
+
slot.value = resolved;
|
|
47
|
+
scheduleUpdate(inst);
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
return [slot.value, slot.set];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ---- useReducer ----
|
|
54
|
+
|
|
55
|
+
export function useReducer(reducer, initialArg, init) {
|
|
56
|
+
const inst = _requireInstance('useReducer');
|
|
57
|
+
const slot = _getHookSlot(inst);
|
|
58
|
+
if (!slot.init) {
|
|
59
|
+
slot.init = true;
|
|
60
|
+
slot.value = init ? init(initialArg) : initialArg;
|
|
61
|
+
slot.reducer = reducer;
|
|
62
|
+
slot.dispatch = (action) => {
|
|
63
|
+
const next = slot.reducer(slot.value, action);
|
|
64
|
+
if (Object.is(next, slot.value)) return;
|
|
65
|
+
slot.value = next;
|
|
66
|
+
scheduleUpdate(inst);
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
slot.reducer = reducer; // always use the latest reducer closure
|
|
70
|
+
return [slot.value, slot.dispatch];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ---- useMemo / useCallback ----
|
|
74
|
+
|
|
75
|
+
export function useMemo(factory, deps) {
|
|
76
|
+
const inst = _requireInstance('useMemo');
|
|
77
|
+
const slot = _getHookSlot(inst);
|
|
78
|
+
if (!slot.init || depsChanged(slot.deps, deps)) {
|
|
79
|
+
slot.init = true;
|
|
80
|
+
slot.deps = deps;
|
|
81
|
+
slot.value = factory();
|
|
82
|
+
}
|
|
83
|
+
return slot.value;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function useCallback(callback, deps) {
|
|
87
|
+
const inst = _requireInstance('useCallback');
|
|
88
|
+
const slot = _getHookSlot(inst);
|
|
89
|
+
if (!slot.init || depsChanged(slot.deps, deps)) {
|
|
90
|
+
slot.init = true;
|
|
91
|
+
slot.deps = deps;
|
|
92
|
+
slot.value = callback;
|
|
93
|
+
}
|
|
94
|
+
return slot.value;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ---- useRef ----
|
|
98
|
+
|
|
99
|
+
export function useRef(initial) {
|
|
100
|
+
const inst = _requireInstance('useRef');
|
|
101
|
+
const slot = _getHookSlot(inst);
|
|
102
|
+
if (!slot.init) {
|
|
103
|
+
slot.init = true;
|
|
104
|
+
slot.value = { current: initial };
|
|
105
|
+
}
|
|
106
|
+
return slot.value;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ---- useEffect / useLayoutEffect / useInsertionEffect ----
|
|
110
|
+
|
|
111
|
+
function useEffectImpl(hookName, push, fn, deps) {
|
|
112
|
+
const inst = _requireInstance(hookName);
|
|
113
|
+
const slot = _getHookSlot(inst);
|
|
114
|
+
if (!slot.init) {
|
|
115
|
+
slot.init = true;
|
|
116
|
+
slot._isEffect = true;
|
|
117
|
+
slot.cleanup = null;
|
|
118
|
+
slot.deps = undefined;
|
|
119
|
+
slot._pending = null;
|
|
120
|
+
}
|
|
121
|
+
if (depsChanged(slot.deps, deps)) {
|
|
122
|
+
slot.deps = deps;
|
|
123
|
+
push(inst, slot, fn);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function useEffect(fn, deps) {
|
|
128
|
+
useEffectImpl('useEffect', _pushPassive, fn, deps);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function useLayoutEffect(fn, deps) {
|
|
132
|
+
useEffectImpl('useLayoutEffect', _pushLayout, fn, deps);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Runs synchronously during render (before this component's DOM mutations) —
|
|
136
|
+
// the closest approximation of React's insertion phase for CSS-in-JS libs.
|
|
137
|
+
export function useInsertionEffect(fn, deps) {
|
|
138
|
+
const inst = _requireInstance('useInsertionEffect');
|
|
139
|
+
const slot = _getHookSlot(inst);
|
|
140
|
+
if (!slot.init) {
|
|
141
|
+
slot.init = true;
|
|
142
|
+
slot._isEffect = true;
|
|
143
|
+
slot.cleanup = null;
|
|
144
|
+
slot.deps = undefined;
|
|
145
|
+
}
|
|
146
|
+
if (depsChanged(slot.deps, deps)) {
|
|
147
|
+
slot.deps = deps;
|
|
148
|
+
if (slot.cleanup) {
|
|
149
|
+
try { slot.cleanup(); } catch (e) { console.error('[what-react] insertion effect cleanup error:', e); }
|
|
150
|
+
slot.cleanup = null;
|
|
151
|
+
}
|
|
152
|
+
try {
|
|
153
|
+
const result = fn();
|
|
154
|
+
if (typeof result === 'function') slot.cleanup = result;
|
|
155
|
+
} catch (e) {
|
|
156
|
+
console.error('[what-react] insertion effect error:', e);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ---- useImperativeHandle ----
|
|
162
|
+
|
|
163
|
+
export function useImperativeHandle(ref, createHandle, deps) {
|
|
164
|
+
useLayoutEffect(() => {
|
|
165
|
+
if (typeof ref === 'function') {
|
|
166
|
+
const handle = createHandle();
|
|
167
|
+
ref(handle);
|
|
168
|
+
return () => ref(null);
|
|
169
|
+
} else if (ref && typeof ref === 'object') {
|
|
170
|
+
ref.current = createHandle();
|
|
171
|
+
return () => { ref.current = null; };
|
|
172
|
+
}
|
|
173
|
+
}, deps == null ? deps : [...deps, ref]);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ---- useContext / createContext ----
|
|
177
|
+
|
|
178
|
+
export function createContext(defaultValue) {
|
|
179
|
+
const context = {
|
|
180
|
+
$$typeof: Symbol.for('react.context'),
|
|
181
|
+
_defaultValue: defaultValue,
|
|
182
|
+
displayName: 'Context',
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
function Provider(props) {
|
|
186
|
+
const inst = _requireInstance('Context.Provider');
|
|
187
|
+
if (!inst._ctxProvided) inst._ctxProvided = new Map();
|
|
188
|
+
const had = inst._ctxProvided.has(context);
|
|
189
|
+
const prev = inst._ctxProvided.get(context);
|
|
190
|
+
inst._ctxProvided.set(context, props.value);
|
|
191
|
+
if (had && !Object.is(prev, props.value) && inst._ctxSubs) {
|
|
192
|
+
// Context propagation: consumers re-render even if intermediate
|
|
193
|
+
// components bailed out (identical-element / memo skip).
|
|
194
|
+
const subs = inst._ctxSubs.get(context);
|
|
195
|
+
if (subs) {
|
|
196
|
+
for (const sub of subs) scheduleUpdate(sub);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return props.children;
|
|
200
|
+
}
|
|
201
|
+
Provider.displayName = 'Context.Provider';
|
|
202
|
+
Provider._context = context;
|
|
203
|
+
|
|
204
|
+
function Consumer(props) {
|
|
205
|
+
const value = useContext(context);
|
|
206
|
+
const children = props.children;
|
|
207
|
+
return typeof children === 'function' ? children(value) : children;
|
|
208
|
+
}
|
|
209
|
+
Consumer.displayName = 'Context.Consumer';
|
|
210
|
+
Consumer._context = context;
|
|
211
|
+
|
|
212
|
+
context.Provider = Provider;
|
|
213
|
+
context.Consumer = Consumer;
|
|
214
|
+
return context;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export function useContext(context) {
|
|
218
|
+
const inst = _requireInstance('useContext');
|
|
219
|
+
let p = inst.parent;
|
|
220
|
+
while (p) {
|
|
221
|
+
if (p._ctxProvided && p._ctxProvided.has(context)) {
|
|
222
|
+
// Subscribe for direct propagation (needed when ancestors bail out).
|
|
223
|
+
if (!p._ctxSubs) p._ctxSubs = new Map();
|
|
224
|
+
let subs = p._ctxSubs.get(context);
|
|
225
|
+
if (!subs) {
|
|
226
|
+
subs = new Set();
|
|
227
|
+
p._ctxSubs.set(context, subs);
|
|
228
|
+
}
|
|
229
|
+
if (!subs.has(inst)) {
|
|
230
|
+
subs.add(inst);
|
|
231
|
+
(inst._ctxDeps || (inst._ctxDeps = [])).push([p, context]);
|
|
232
|
+
}
|
|
233
|
+
return p._ctxProvided.get(context);
|
|
234
|
+
}
|
|
235
|
+
p = p.parent;
|
|
236
|
+
}
|
|
237
|
+
return context._defaultValue;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// ---- useSyncExternalStore ----
|
|
241
|
+
// Spec-compliant: returns the snapshot VALUE and re-renders on store change.
|
|
242
|
+
|
|
243
|
+
export function useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot) {
|
|
244
|
+
const inst = _requireInstance('useSyncExternalStore');
|
|
245
|
+
const slot = _getHookSlot(inst);
|
|
246
|
+
if (!slot.init) {
|
|
247
|
+
slot.init = true;
|
|
248
|
+
slot._isEffect = true; // unmount runs slot.cleanup (the unsubscribe)
|
|
249
|
+
slot.cleanup = null;
|
|
250
|
+
slot.subscribe = undefined;
|
|
251
|
+
slot._pending = null;
|
|
252
|
+
}
|
|
253
|
+
const value = getSnapshot();
|
|
254
|
+
slot.value = value;
|
|
255
|
+
slot.getSnapshot = getSnapshot;
|
|
256
|
+
|
|
257
|
+
if (slot.subscribe !== subscribe) {
|
|
258
|
+
slot.subscribe = subscribe;
|
|
259
|
+
_pushLayout(inst, slot, () => {
|
|
260
|
+
const handleChange = () => {
|
|
261
|
+
const next = slot.getSnapshot();
|
|
262
|
+
if (!Object.is(slot.value, next)) {
|
|
263
|
+
slot.value = next;
|
|
264
|
+
scheduleUpdate(inst);
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
const unsubscribe = subscribe(handleChange);
|
|
268
|
+
handleChange(); // catch changes between render and subscription
|
|
269
|
+
return unsubscribe;
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return value;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// ---- useTransition / useDeferredValue / startTransition ----
|
|
277
|
+
// Rendering here is synchronous — transitions degrade to immediate updates.
|
|
278
|
+
|
|
279
|
+
export function useTransition() {
|
|
280
|
+
_requireInstance('useTransition');
|
|
281
|
+
return [false, startTransition];
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export function startTransition(fn) {
|
|
285
|
+
fn();
|
|
286
|
+
flushUpdates();
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export function useDeferredValue(value) {
|
|
290
|
+
return value;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// ---- useId ----
|
|
294
|
+
|
|
295
|
+
let idCounter = 0;
|
|
296
|
+
|
|
297
|
+
export function useId() {
|
|
298
|
+
const inst = _requireInstance('useId');
|
|
299
|
+
const slot = _getHookSlot(inst);
|
|
300
|
+
if (!slot.init) {
|
|
301
|
+
slot.init = true;
|
|
302
|
+
slot.value = ':w' + (++idCounter).toString(36) + ':';
|
|
303
|
+
}
|
|
304
|
+
return slot.value;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// ---- useDebugValue ----
|
|
308
|
+
|
|
309
|
+
export function useDebugValue() {}
|
|
310
|
+
|
|
311
|
+
// ---- use (React 19-style, minimal) ----
|
|
312
|
+
// Context → useContext. Thenable → resolved value or throw for Suspense.
|
|
313
|
+
|
|
314
|
+
export function use(usable) {
|
|
315
|
+
if (usable !== null && typeof usable === 'object') {
|
|
316
|
+
if (typeof usable.then === 'function') {
|
|
317
|
+
const thenable = usable;
|
|
318
|
+
if (thenable._whatStatus === 'fulfilled') return thenable._whatValue;
|
|
319
|
+
if (thenable._whatStatus === 'rejected') throw thenable._whatReason;
|
|
320
|
+
if (thenable._whatStatus === undefined) {
|
|
321
|
+
thenable._whatStatus = 'pending';
|
|
322
|
+
thenable.then(
|
|
323
|
+
(v) => { thenable._whatStatus = 'fulfilled'; thenable._whatValue = v; },
|
|
324
|
+
(e) => { thenable._whatStatus = 'rejected'; thenable._whatReason = e; },
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
throw thenable; // caught by the nearest Suspense boundary
|
|
328
|
+
}
|
|
329
|
+
if (usable.$$typeof === Symbol.for('react.context')) {
|
|
330
|
+
return useContext(usable);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
throw new Error('[what-react] use() expects a promise or a context.');
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// ---- useSignal (escape hatch) ----
|
|
337
|
+
// Bridge helper for mixed codebases: subscribe a compat component to a
|
|
338
|
+
// what-core signal. Re-renders this component when the signal changes.
|
|
339
|
+
|
|
340
|
+
export function useWhatSignal(sig) {
|
|
341
|
+
return useSyncExternalStore(
|
|
342
|
+
(notify) => {
|
|
343
|
+
// what-core signals don't expose subscribe directly; poll via effect-free
|
|
344
|
+
// microtask comparison is wasteful — use sig.subscribe if present.
|
|
345
|
+
if (typeof sig.subscribe === 'function') return sig.subscribe(notify);
|
|
346
|
+
// Fallback: no subscription available; value still read each render.
|
|
347
|
+
return () => {};
|
|
348
|
+
},
|
|
349
|
+
() => sig(),
|
|
350
|
+
);
|
|
351
|
+
}
|