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 CHANGED
@@ -1,8 +1,8 @@
1
1
  # what-react
2
2
 
3
- React compatibility layer for [What Framework](https://whatfw.com). Use React ecosystem libraries with What's signal-based engine under the hood -- zero code changes required.
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
- **90+ React libraries confirmed working**, including zustand, @tanstack/react-query, react-hook-form, framer-motion, @radix-ui, react-select, react-router, and many more.
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` implements React's public API using What's signals and reconciler:
78
+ `what-react` ships its own React-semantics runtime (`src/runtime.js`) what-core is not modified:
79
79
 
80
- - `useState`, `useEffect`, `useMemo`, etc. map to What's hook system
81
- - `createElement` maps to What's `h()` hyperscript
82
- - Class components (`Component`, `PureComponent`) are wrapped as function components
83
- - `createRoot` / `render` map to What's `mount()`
84
- - `createPortal` creates portal vnodes handled by What's reconciler
85
- - `forwardRef`, `cloneElement`, `Children`, `createContext` all implemented
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
- The key insight: React libraries import `react` and call its hooks. By aliasing `react` to `what-react`, those hooks execute on What's signal engine instead. The library never knows the difference.
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.10.0",
4
- "description": "React compatibility layer for What Framework — use React packages with signals under the hood",
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.10.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
- * Implements ReactDOM's public API using What's mount() and rendering.
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 { mount as whatMount, h, Fragment } from 'what-core';
8
- import { flushSync as whatFlushSync } from 'what-core';
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 unmount = null;
20
+ let root = null;
14
21
 
15
22
  return {
16
23
  render(element) {
17
- if (unmount) unmount();
18
- unmount = whatMount(element, container);
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 (unmount) {
22
- unmount();
23
- unmount = null;
32
+ if (root) {
33
+ unmountRoot(root);
34
+ root = null;
24
35
  }
25
- container.innerHTML = '';
36
+ container.textContent = '';
26
37
  },
27
38
  };
28
39
  }
29
40
 
30
41
  // ---- hydrateRoot ----
31
- // Basic implementationmounts fresh (true hydration would reuse existing DOM)
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
- const root = createRoot(container);
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
- // Create a vnode that the core reconciler recognizes as a portal.
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 || null,
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
- if (fn) fn();
76
- whatFlushSync();
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
- // If it's already a DOM node, return it
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
- // If the component has a ref attached, try that
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
+ }