@pyreon/react-compat 0.1.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/src/index.ts ADDED
@@ -0,0 +1,276 @@
1
+ /**
2
+ * @pyreon/react-compat
3
+ *
4
+ * React-compatible hook API that runs on Pyreon's reactive engine.
5
+ *
6
+ * Allows you to write familiar React-style code while getting Pyreon's
7
+ * fine-grained reactivity, built-in router/store, and superior performance.
8
+ *
9
+ * DIFFERENCES FROM REACT:
10
+ * - No hooks rules: call these anywhere in a component, in loops, conditions, etc.
11
+ * - useEffect deps array is IGNORED — Pyreon tracks dependencies automatically.
12
+ * - useCallback/memo are identity functions — no re-renders means no stale closures.
13
+ * - Components run ONCE (setup), not on every render.
14
+ *
15
+ * USAGE:
16
+ * Replace `import { useState, useEffect } from "react"` with
17
+ * `import { useState, useEffect } from "@pyreon/react-compat"`
18
+ * Replace `import { createRoot } from "react-dom/client"` with
19
+ * `import { createRoot } from "@pyreon/react-compat/dom"`
20
+ */
21
+
22
+ export type { Props, VNode as ReactNode, VNodeChild } from "@pyreon/core"
23
+ // Re-export Pyreon's JSX runtime so JSX transforms work the same way
24
+ // Lifecycle
25
+ export { Fragment, h as createElement, h, onMount as useLayoutEffect } from "@pyreon/core"
26
+
27
+ import type { CleanupFn, VNodeChild } from "@pyreon/core"
28
+ import {
29
+ createContext,
30
+ createRef,
31
+ ErrorBoundary,
32
+ onErrorCaptured,
33
+ onMount,
34
+ onUnmount,
35
+ onUpdate,
36
+ Portal,
37
+ Suspense,
38
+ useContext,
39
+ } from "@pyreon/core"
40
+ import {
41
+ batch,
42
+ computed,
43
+ createSelector,
44
+ effect,
45
+ getCurrentScope,
46
+ runUntracked,
47
+ signal,
48
+ } from "@pyreon/reactivity"
49
+
50
+ // ─── State ────────────────────────────────────────────────────────────────────
51
+
52
+ /**
53
+ * Drop-in for React's `useState`.
54
+ * Returns `[getter, setter]` — call `getter()` to read, `setter(v)` to write.
55
+ *
56
+ * Unlike React: the getter is a signal, so any component or effect that reads
57
+ * it will re-run automatically. No dep arrays needed.
58
+ */
59
+ export function useState<T>(initial: T | (() => T)): [() => T, (v: T | ((prev: T) => T)) => void] {
60
+ const s = signal<T>(typeof initial === "function" ? (initial as () => T)() : initial)
61
+ const setter = (v: T | ((prev: T) => T)) => {
62
+ if (typeof v === "function") s.update(v as (prev: T) => T)
63
+ else s.set(v)
64
+ }
65
+ return [s, setter]
66
+ }
67
+
68
+ // ─── Reducer ─────────────────────────────────────────────────────────────────
69
+
70
+ /**
71
+ * Drop-in for React's `useReducer`.
72
+ */
73
+ export function useReducer<S, A>(
74
+ reducer: (state: S, action: A) => S,
75
+ initial: S | (() => S),
76
+ ): [() => S, (action: A) => void] {
77
+ const s = signal<S>(typeof initial === "function" ? (initial as () => S)() : initial)
78
+ const dispatch = (action: A) => s.update((prev) => reducer(prev, action))
79
+ return [s, dispatch]
80
+ }
81
+
82
+ // ─── Effects ─────────────────────────────────────────────────────────────────
83
+
84
+ /**
85
+ * Drop-in for React's `useEffect`.
86
+ *
87
+ * The `deps` array is IGNORED — Pyreon tracks reactive dependencies automatically.
88
+ * If `deps` is `[]` (mount-only), wrap the body in `runUntracked(() => ...)`.
89
+ *
90
+ * Returns a cleanup the same way React does (return a function from `fn`).
91
+ */
92
+ // biome-ignore lint/suspicious/noConfusingVoidType: void is intentional — callers may return void
93
+ export function useEffect(fn: () => CleanupFn | void, deps?: unknown[]): void {
94
+ if (deps !== undefined && deps.length === 0) {
95
+ // [] means "run once on mount" — use onMount instead of a tracking effect
96
+ onMount((): undefined => {
97
+ const cleanup = runUntracked(fn)
98
+ if (typeof cleanup === "function") onUnmount(cleanup)
99
+ })
100
+ } else {
101
+ // No deps or non-empty deps: run reactively (Pyreon auto-tracks).
102
+ // effect() natively supports cleanup: if fn() returns a function,
103
+ // it's called before re-runs and on dispose.
104
+ const e = effect(fn)
105
+ onUnmount(() => {
106
+ e.dispose()
107
+ })
108
+ }
109
+ }
110
+
111
+ /**
112
+ * Drop-in for React's `useLayoutEffect`.
113
+ * In Pyreon there is no paint distinction — maps to `onMount` (same as useEffect).
114
+ */
115
+ export { useEffect as useLayoutEffect_ }
116
+
117
+ // ─── Memoization ─────────────────────────────────────────────────────────────
118
+
119
+ /**
120
+ * Drop-in for React's `useMemo`.
121
+ * The `deps` array is IGNORED — Pyreon's `computed` tracks dependencies automatically.
122
+ * Returns a getter: call `value()` to read the memoized result.
123
+ */
124
+ export function useMemo<T>(fn: () => T, _deps?: unknown[]): () => T {
125
+ return computed(fn)
126
+ }
127
+
128
+ /**
129
+ * Drop-in for React's `useCallback`.
130
+ * In Pyreon, components run once so callbacks are never recreated — returns `fn` as-is.
131
+ */
132
+ export function useCallback<T extends (...args: unknown[]) => unknown>(
133
+ fn: T,
134
+ _deps?: unknown[],
135
+ ): T {
136
+ return fn
137
+ }
138
+
139
+ // ─── Refs ─────────────────────────────────────────────────────────────────────
140
+
141
+ /**
142
+ * Drop-in for React's `useRef`.
143
+ * Returns `{ current: T }` — same shape as React's ref object.
144
+ */
145
+ export function useRef<T>(initial?: T): { current: T | null } {
146
+ const ref = createRef<T>()
147
+ if (initial !== undefined) ref.current = initial as T
148
+ return ref
149
+ }
150
+
151
+ // ─── Context ─────────────────────────────────────────────────────────────────
152
+
153
+ /**
154
+ * Drop-in for React's `createContext` + `useContext`.
155
+ * Usage mirrors React: `const Ctx = createContext(defaultValue)`.
156
+ */
157
+ export { createContext, useContext }
158
+
159
+ // ─── ID ───────────────────────────────────────────────────────────────────────
160
+
161
+ /**
162
+ * Drop-in for React's `useId` — returns a stable unique string per component instance.
163
+ *
164
+ * Uses the component's effectScope as the key so the counter starts at 0 for every
165
+ * component on both server and client — IDs are deterministic and hydration-safe.
166
+ */
167
+ const _idCounters = new WeakMap<object, number>()
168
+
169
+ export function useId(): string {
170
+ const scope = getCurrentScope()
171
+ if (!scope) return `:r${Math.random().toString(36).slice(2, 9)}:`
172
+ const count = _idCounters.get(scope) ?? 0
173
+ _idCounters.set(scope, count + 1)
174
+ return `:r${count.toString(36)}:`
175
+ }
176
+
177
+ // ─── Optimization ─────────────────────────────────────────────────────────────
178
+
179
+ /**
180
+ * Drop-in for React's `memo` — wraps a component.
181
+ * In Pyreon, components run once (no re-renders), so memoization is a no-op.
182
+ * Kept for API compatibility when migrating React code.
183
+ */
184
+ export function memo<P extends Record<string, unknown>>(
185
+ component: (props: P) => VNodeChild,
186
+ ): (props: P) => VNodeChild {
187
+ return component
188
+ }
189
+
190
+ /**
191
+ * Drop-in for React's `useTransition` — no-op in Pyreon (no concurrent mode).
192
+ * Returns `[false, (fn) => fn()]` to keep code runnable without changes.
193
+ */
194
+ export function useTransition(): [boolean, (fn: () => void) => void] {
195
+ return [false, (fn) => fn()]
196
+ }
197
+
198
+ /**
199
+ * Drop-in for React's `useDeferredValue` — returns the value as-is in Pyreon.
200
+ */
201
+ export function useDeferredValue<T>(value: T): T {
202
+ return value
203
+ }
204
+
205
+ // ─── Batching ─────────────────────────────────────────────────────────────────
206
+
207
+ /**
208
+ * Drop-in for React's `unstable_batchedUpdates` / React 18's automatic batching.
209
+ * Pyreon's `batch()` does the same thing.
210
+ */
211
+ export { batch }
212
+
213
+ // ─── Error boundaries ─────────────────────────────────────────────────────────
214
+
215
+ /**
216
+ * Drop-in for React's error boundary pattern.
217
+ * Return `true` from `handler` to prevent error propagation (like `componentDidCatch`).
218
+ */
219
+ export { onErrorCaptured as useErrorBoundary }
220
+
221
+ // ─── Portals ─────────────────────────────────────────────────────────────────
222
+
223
+ /**
224
+ * Drop-in for React's `createPortal(children, target)`.
225
+ */
226
+ export function createPortal(children: VNodeChild, target: Element): VNodeChild {
227
+ return Portal({ target, children })
228
+ }
229
+
230
+ // ─── Imperative handle ────────────────────────────────────────────────────────
231
+
232
+ /**
233
+ * Drop-in for React's `useImperativeHandle`.
234
+ * In Pyreon, expose methods via a ref prop directly — this is a compatibility shim.
235
+ */
236
+ export function useImperativeHandle<T>(
237
+ ref: { current: T | null } | null | undefined,
238
+ init: () => T,
239
+ _deps?: unknown[],
240
+ ): void {
241
+ onMount((): undefined => {
242
+ if (ref) ref.current = init()
243
+ })
244
+ onUnmount(() => {
245
+ if (ref) ref.current = null
246
+ })
247
+ }
248
+
249
+ // ─── Selector ─────────────────────────────────────────────────────────────────
250
+
251
+ /**
252
+ * Pyreon-specific: O(1) equality selector (no React equivalent).
253
+ * Useful for large lists where only the selected item should re-render.
254
+ * @see createSelector in @pyreon/reactivity
255
+ */
256
+ export { createSelector }
257
+
258
+ // ─── onUpdate ─────────────────────────────────────────────────────────────────
259
+
260
+ /** Pyreon-specific lifecycle hook — runs after each reactive update. */
261
+ export { onMount, onUnmount, onUpdate }
262
+
263
+ // ─── Suspense / lazy ──────────────────────────────────────────────────────────
264
+
265
+ /**
266
+ * Drop-in for React's `lazy()`.
267
+ * Re-exported from `@pyreon/core` — wraps a dynamic import, renders null until
268
+ * the module resolves. Pair with `<Suspense>` to show a fallback during loading.
269
+ */
270
+ export { lazy } from "@pyreon/core"
271
+
272
+ /**
273
+ * Drop-in for React's `<Suspense>`.
274
+ * Shows `fallback` while a `lazy()` child is still loading.
275
+ */
276
+ export { Suspense, ErrorBoundary }