vaderjs 2.2.5 → 2.3.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/index.ts CHANGED
@@ -1,805 +1,925 @@
1
- //@ts-nocheck
2
- let isClassComponent = function (element) {
3
- return element.toString().startsWith("class");
4
- };
1
+ /**
2
+ * @file A lightweight React-like library with hooks implementation.
3
+ * @module vader
4
+ */
5
5
 
6
-
7
-
8
- const memoizes = new Map();
9
- //@ts-ignore
10
-
11
- declare global {
12
- interface Window {
13
- onbeforeunload: any;
14
- localStorage: any;
15
- sessionStorage: any;
16
- state: any;
17
- }
18
- const genKey: any;
19
- /**
20
- * @description Allows you to check if current session is server or client
21
- */
22
- let isServer: boolean;
23
- /**
24
- * @description - The params object is used to store the parameters of the current URL
25
- * @example
26
- * // URL: https://example.com?name=John
27
- * console.log(params.name) // John
28
- * @example
29
- * // URL: https://example.com/:name/:age
30
- * // GO: https://example.com/John/20
31
- * console.log(params.name) // John
32
- * console.log(params.age) // 20
33
- */
34
- let params: { [key: string]: string };
35
- let localStorage: []
36
- }
37
- //@ts-ignore
38
- globalThis.isServer = typeof window === "undefined";
39
- //@ts-ignore
40
- if(isServer){
41
- globalThis.params = {
42
- [Symbol.iterator]: function* () {
43
- for (const key in this) {
44
- yield [key, this[key]];
45
- }
46
- },
6
+ /**
7
+ * Global variables for the fiber tree and rendering process.
8
+ */
9
+ let nextUnitOfWork: Fiber | null = null;
10
+ let wipRoot: Fiber | null = null;
11
+ let currentRoot: Fiber | null = null;
12
+ let deletions: Fiber[] | null = null;
13
+ let wipFiber: Fiber | null = null;
14
+ let hookIndex = 0;
15
+ let isRenderScheduled = false;
16
+
17
+ interface Fiber {
18
+ type?: string | Function;
19
+ dom?: Node;
20
+ props: {
21
+ children: VNode[];
22
+ [key: string]: any;
47
23
  };
24
+ parent?: Fiber;
25
+ child?: Fiber;
26
+ sibling?: Fiber;
27
+ alternate?: Fiber;
28
+ effectTag?: "PLACEMENT" | "UPDATE" | "DELETION";
29
+ hooks?: Hook[];
30
+ key?: string | number | null;
48
31
  }
49
32
 
33
+ interface VNode {
34
+ type: string | Function;
35
+ props: {
36
+ children: VNode[];
37
+ [key: string]: any;
38
+ };
39
+ key?: string | number | null;
40
+ }
50
41
 
42
+ interface Hook {
43
+ state?: any;
44
+ queue?: any[];
45
+ deps?: any[];
46
+ _cleanupFn?: Function;
47
+ memoizedValue?: any;
48
+ current?: any;
49
+ }
51
50
 
52
51
  /**
53
- * @description useFetch allows you to make POST - GET - PUT - DELETE requests then returns the data, loading state and error
54
- * @param url
55
- * @param options
56
- * @returns [data, loading, error]
52
+ * Checks if a property key is an event handler.
53
+ * @param {string} key - The property key to check.
54
+ * @returns {boolean} True if the key is an event handler.
57
55
  */
58
- export const useFetch = (url: string, options: any) => {
59
- return [null, true, null];
60
- };
56
+ const isEvent = (key: string) => key.startsWith("on");
57
+
61
58
  /**
62
- * @description - Bypasses this error when using state in a non parent function
63
- * @param funct
64
- * @param context
65
- * @returns
66
- * @example
67
- * // - src/index.ts
68
- *
69
- * export default function useAuth(){
70
- * let [isAuthenticated, setAuthenticated] = useState(false)
71
- * }
72
- *
73
- * // app/index.jsx
74
- *
75
- * export default function(){
76
- * // this will error because this is not present in the cild function
77
- * const { isAuthenticated } = useAuth()
78
- * // to declare this we need to use bound from vaderjs module
79
- * const { isAuthenticated } = bound(useAuth)()
80
- * return (
81
- * <div></div>
82
- *
83
- * )
84
- * }
85
- */
86
- export function bound(funct: Function, context: any) {
87
- return function() {
88
- return funct.apply(context, arguments);
89
- };
59
+ * Checks if a property key is a regular property (not children or event).
60
+ * @param {string} key - The property key to check.
61
+ * @returns {boolean} True if the key is a regular property.
62
+ */
63
+ const isProperty = (key: string) => key !== "children" && !isEvent(key);
64
+
65
+ /**
66
+ * Creates a function to check if a property has changed between objects.
67
+ * @param {object} prev - The previous object.
68
+ * @param {object} next - The next object.
69
+ * @returns {function} A function that takes a key and returns true if the property changed.
70
+ */
71
+ const isNew = (prev: object, next: object) => (key: string) => prev[key] !== next[key];
72
+
73
+ /**
74
+ * Creates a function to check if a property was removed from an object.
75
+ * @param {object} prev - The previous object.
76
+ * @param {object} next - The next object.
77
+ * @returns {function} A function that takes a key and returns true if the property was removed.
78
+ */
79
+ const isGone = (prev: object, next: object) => (key: string) => !(key in next);
80
+
81
+ /**
82
+ * Creates a DOM node for a fiber.
83
+ * @param {Fiber} fiber - The fiber to create a DOM node for.
84
+ * @returns {Node} The created DOM node.
85
+ */
86
+ function createDom(fiber: Fiber): Node {
87
+ const dom =
88
+ fiber.type == "TEXT_ELEMENT"
89
+ ? document.createTextNode("")
90
+ : document.createElement(fiber.type as string);
91
+
92
+ updateDom(dom, {}, fiber.props);
93
+ return dom;
90
94
  }
95
+
91
96
  /**
92
- * @description - useRef allows you to store a reference to a DOM element
93
- * @param value
94
- * @returns {current: HTMLElement}
95
- * @example
96
- * const inputRef = useRef();
97
- * <input ref={inputRef} />
98
- * console.log(inputRef.current) // <input />
97
+ * Applies updated props to a DOM node.
98
+ * @param {Node} dom - The DOM node to update.
99
+ * @param {object} prevProps - The previous properties.
100
+ * @param {object} nextProps - The new properties.
99
101
  */
100
- export const useRef = (value) => {
101
- return { key: crypto.randomUUID(), current: value };
102
+ function updateDom(dom: Node, prevProps: object, nextProps: object): void {
103
+ prevProps = prevProps || {};
104
+ nextProps = nextProps || {};
105
+
106
+ // Remove old or changed event listeners
107
+ Object.keys(prevProps)
108
+ .filter(isEvent)
109
+ .filter((key) => !(key in nextProps) || isNew(prevProps, nextProps)(key))
110
+ .forEach((name) => {
111
+ const eventType = name.toLowerCase().substring(2);
112
+ if (typeof prevProps[name] === 'function') {
113
+ dom.removeEventListener(eventType, prevProps[name]);
114
+ }
115
+ });
116
+
117
+ // Remove old properties
118
+ Object.keys(prevProps)
119
+ .filter(isProperty)
120
+ .filter(isGone(prevProps, nextProps))
121
+ .forEach((name) => {
122
+ // FIX: Handle both `class` and `className`
123
+ if (name === 'className' || name === 'class') {
124
+ (dom as HTMLElement).removeAttribute("class");
125
+ } else {
126
+ dom[name] = "";
127
+ }
128
+ });
129
+
130
+ // Set new or changed properties
131
+ Object.keys(nextProps)
132
+ .filter(isProperty)
133
+ .filter(isNew(prevProps, nextProps))
134
+ .forEach((name) => {
135
+ if (name === 'style' && typeof nextProps[name] === 'string') {
136
+ (dom as HTMLElement).style.cssText = nextProps[name];
137
+ } else if (name === 'className' || name === 'class') {
138
+ // FIX: Handle both `class` and `className`
139
+ (dom as HTMLElement).className = nextProps[name];
140
+ } else {
141
+ dom[name] = nextProps[name];
142
+ }
143
+ });
144
+
145
+ // Add new event listeners
146
+ Object.keys(nextProps)
147
+ .filter(isEvent)
148
+ .filter(isNew(prevProps, nextProps))
149
+ .forEach((name) => {
150
+ const eventType = name.toLowerCase().substring(2);
151
+ const handler = nextProps[name];
152
+ if (typeof handler === 'function') {
153
+ dom.addEventListener(eventType, handler);
154
+ }
155
+ });
102
156
  }
103
157
 
104
158
  /**
105
- * @description - Handle asyncronous promises and return the data or error;
106
- * @param promise
107
- * @returns
159
+ * Commits the entire work-in-progress tree to the DOM.
108
160
  */
109
- export const useAsyncState = (promise: Promise<any>) => {
110
- return [null, () => { }];
161
+ function commitRoot(): void {
162
+ deletions.forEach(commitWork);
163
+ commitWork(wipRoot.child);
164
+ currentRoot = wipRoot;
165
+ wipRoot = null;
166
+ isRenderScheduled = false;
111
167
  }
112
- export const useEffect = (callback: any, dependencies: any[] = []) => {
113
- dependencies = dependencies.map((dep) => JSON.stringify(dep));
114
- if (dependencies.length === 0) {
115
- callback();
168
+
169
+ /**
170
+ * Recursively commits a fiber and its children to the DOM.
171
+ * @param {Fiber} fiber - The fiber to commit.
172
+ */
173
+ function commitWork(fiber: Fiber | null): void {
174
+ if (!fiber) {
175
+ return;
176
+ }
177
+
178
+ let domParentFiber = fiber.parent;
179
+ while (domParentFiber && !domParentFiber.dom) {
180
+ domParentFiber = domParentFiber.parent;
181
+ }
182
+ const domParent = domParentFiber ? domParentFiber.dom : null;
183
+
184
+ if (fiber.effectTag === "PLACEMENT" && fiber.dom != null) {
185
+ if (domParent) domParent.appendChild(fiber.dom);
186
+ } else if (fiber.effectTag === "UPDATE" && fiber.dom != null) {
187
+ updateDom(fiber.dom, fiber.alternate?.props ?? {}, fiber.props);
188
+ } else if (fiber.effectTag === "DELETION") {
189
+ commitDeletion(fiber);
190
+ }
191
+
192
+ commitWork(fiber.child);
193
+ commitWork(fiber.sibling);
194
+ }
195
+
196
+ /**
197
+ * Recursively removes a fiber and its children from the DOM.
198
+ * @param {Fiber} fiber - The fiber to remove.
199
+ */
200
+ function commitDeletion(fiber: Fiber | null): void {
201
+ if (!fiber) {
202
+ return;
203
+ }
204
+ if (fiber.dom) {
205
+ if (fiber.dom.parentNode) {
206
+ fiber.dom.parentNode.removeChild(fiber.dom);
207
+ }
208
+ } else if (fiber.child) {
209
+ commitDeletion(fiber.child);
210
+ }
211
+ }
212
+
213
+ /**
214
+ * Renders a virtual DOM element into a container.
215
+ * @param {VNode} element - The root virtual DOM element to render.
216
+ * @param {Node} container - The DOM container to render into.
217
+ */
218
+ export function render(element: VNode, container: Node): void {
219
+ container.innerHTML = "";
220
+
221
+ wipRoot = {
222
+ dom: container,
223
+ props: {
224
+ children: [element],
225
+ },
226
+ alternate: currentRoot,
227
+ };
228
+ deletions = [];
229
+ nextUnitOfWork = wipRoot;
230
+
231
+ if (!isRenderScheduled) {
232
+ isRenderScheduled = true;
233
+ requestAnimationFrame(workLoop);
116
234
  }
117
235
  }
118
236
 
119
- // make a switch function component
237
+ /**
238
+ * The main work loop for rendering and reconciliation.
239
+ */
240
+ function workLoop(): void {
241
+ if (!wipRoot && currentRoot) {
242
+ wipRoot = {
243
+ dom: currentRoot.dom,
244
+ props: currentRoot.props,
245
+ alternate: currentRoot,
246
+ };
247
+ deletions = [];
248
+ nextUnitOfWork = wipRoot;
249
+ }
120
250
 
251
+ while (nextUnitOfWork) {
252
+ nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
253
+ }
121
254
 
122
- export const A = (props: {
123
- /**
124
- * @description Set the elements classlist
125
- */
126
- className?: string;
127
- /**
128
- * @description Once clicked send user to a different link
129
- */
130
- href?: string;
131
- style?: string;
132
- openInNewTab?: boolean
133
- onClick?: () => void;
134
- onChange?: () => void;
135
- }, children: any) => {
136
- function handleClick(e) {
137
- e.preventDefault();
138
- if (props.openInNewTab) {
139
- window.open(props.href, "_blank");
140
- return void 0;
141
- }
142
- window.history.pushState({}, "", props.href);
143
- window.dispatchEvent(new Event("popstate"));
144
- window.dispatchEvent(new Event("load"));
145
- window.location.reload();
146
- return void 0;
255
+ if (!nextUnitOfWork && wipRoot) {
256
+ commitRoot();
147
257
  }
148
- return e("a", { ...props, onClick: handleClick }, props.children);
149
258
  }
150
259
 
260
+ /**
261
+ * Performs work on a single fiber unit.
262
+ * @param {Fiber} fiber - The fiber to perform work on.
263
+ * @returns {Fiber|null} The next fiber to work on.
264
+ */
265
+ function performUnitOfWork(fiber: Fiber): Fiber | null {
266
+ const isFunctionComponent = fiber.type instanceof Function;
151
267
 
152
- export const Fragment = (props: any, children: any) => {
153
- return {
154
- type: null,
155
- props: props,
156
- children
157
- }
158
- }
159
-
160
- if(typeof window !== "undefined") {
161
- window.history.back = () => {
162
- window.history.go(-1);
163
- }
164
- window.history.forward = () => {
165
- window.history.go(1);
166
- }
167
- }
168
- globalThis.Fragment = Fragment;
169
-
170
- /**
171
- * @description - Create a new element
172
- * @param element
173
- * @param props
174
- * @param children
175
- * @returns
176
- */
177
- export const e = (element, props, ...children) => {
178
- if (!element)
179
- return "";
180
- let instance;
181
- switch (true) {
182
- case isClassComponent(element):
183
- instance = new element;
184
- instance.props = props;
185
- instance.children = children;
186
- instance.Mounted = true;
187
- return instance.render(props);
188
- case typeof element === "function":
189
- instance = memoizeClassComponent(Component, element.name);
190
- element = element.bind(instance);
191
- instance.render = (props) => element(props);
192
- if (element.name.toLowerCase() == "default") {
193
- throw new Error("Function name must be unique");
194
- }
195
- instance.key = element.name;
196
- instance.Mounted = true;
197
- let firstEl = instance.render({ key: instance.key, children, ...props }, children);
198
- instance.children = children;
199
- if (!firstEl)
200
- firstEl = { type: "div", props: { key: instance.key, ...props }, children };
201
- firstEl.props = { key: instance.key, ...firstEl.props, ...props };
202
- firstEl.props["idKey"] = instance.props?.ref?.key || instance.key;
203
- instance.props = firstEl.props;
204
- return firstEl;
205
- default:
206
- if(!element) {
207
- return "";
208
- }
209
- let el = { type: element, props: props || {}, children: children || [] };
210
- if (el.type !== "head") {
211
- el.props = { idKey: el.props?.ref?.key || crypto.randomUUID(), ...el.props };
212
- }
268
+ if (isFunctionComponent) {
269
+ updateFunctionComponent(fiber);
270
+ } else {
271
+ updateHostComponent(fiber);
272
+ }
213
273
 
214
- // if element == false return empty string
215
- if (el.type === false) {
216
- return "";
217
- }
274
+ if (fiber.child) {
275
+ return fiber.child;
276
+ }
218
277
 
219
- return el;
278
+ let nextFiber = fiber;
279
+ while (nextFiber) {
280
+ if (nextFiber.sibling) {
281
+ return nextFiber.sibling;
282
+ }
283
+ nextFiber = nextFiber.parent;
220
284
  }
221
- };
285
+ return null;
286
+ }
222
287
 
223
- /*
224
- * @description - Switch component
225
- * @param element
226
- * @param props
227
- * @param children
228
- * @returns
229
- */
288
+ /**
289
+ * Updates a function component fiber.
290
+ * @param {Fiber} fiber - The function component fiber to update.
291
+ */
292
+ function updateFunctionComponent(fiber: Fiber): void {
293
+ wipFiber = fiber;
294
+ hookIndex = 0;
295
+ wipFiber.hooks = fiber.alternate ? fiber.alternate.hooks : [];
230
296
 
297
+ const children = [fiber.type(fiber.props)]
298
+ .flat()
299
+ .filter(child => child != null && typeof child !== 'boolean')
300
+ .map(child => typeof child === "object" ? child : createTextElement(child));
231
301
 
232
- interface SwitchProps {
233
- children: any[] | any;
302
+ reconcileChildren(fiber, children);
234
303
  }
235
304
 
236
- const acceptedAttributes = [
237
- // Global attributes
238
- 'accesskey', 'class', 'className', 'idKey', 'contenteditable', 'contextmenu', 'data', 'dir', 'hidden',
239
- 'id', 'lang', 'style', 'tabindex', 'title', 'translate', 'xml:lang', 'xml:space',
305
+ /**
306
+ * Updates a host component fiber (DOM element).
307
+ * @param {Fiber} fiber - The host component fiber to update.
308
+ */
309
+ function updateHostComponent(fiber: Fiber): void {
310
+ if (!fiber.dom) {
311
+ fiber.dom = createDom(fiber);
312
+ }
313
+ reconcileChildren(fiber, fiber.props.children);
314
+ }
240
315
 
241
- // SVG-specific attributes
242
- 'xmlns', 'fill', 'viewBox', 'stroke-width', 'stroke', 'd', 'stroke-linecap', 'stroke-linejoin', 'content', 'name'
243
- ];
316
+ /**
317
+ * Reconciles the children of a fiber with new elements.
318
+ * @param {Fiber} wipFiber - The work-in-progress fiber.
319
+ * @param {VNode[]} elements - The new child elements.
320
+ */
321
+ function reconcileChildren(wipFiber: Fiber, elements: VNode[]): void {
322
+ let index = 0;
323
+ let oldFiber = wipFiber.alternate?.child;
324
+ let prevSibling = null;
325
+
326
+ const oldFibersByKey = new Map<string | number, Fiber>();
327
+
328
+ while (oldFiber) {
329
+ const key = oldFiber.key != null ? oldFiber.key : index;
330
+ oldFibersByKey.set(key, oldFiber);
331
+ oldFiber = oldFiber.sibling;
332
+ index++;
333
+ }
244
334
 
335
+ index = 0;
336
+ prevSibling = null;
337
+
338
+ for (; index < elements.length; index++) {
339
+ const element = elements[index];
340
+ const key = element.key != null ? element.key : index;
341
+ const oldFiber = oldFibersByKey.get(key);
342
+ const sameType = oldFiber && element.type === oldFiber.type;
343
+
344
+ let newFiber: Fiber | null = null;
345
+
346
+ if (sameType) {
347
+ newFiber = {
348
+ type: oldFiber.type,
349
+ props: element.props,
350
+ dom: oldFiber.dom,
351
+ parent: wipFiber,
352
+ alternate: oldFiber,
353
+ effectTag: "UPDATE",
354
+ key,
355
+ };
356
+ oldFibersByKey.delete(key);
357
+ } else {
358
+ if (element) {
359
+ newFiber = {
360
+ type: element.type,
361
+ props: element.props,
362
+ dom: null,
363
+ parent: wipFiber,
364
+ alternate: null,
365
+ effectTag: "PLACEMENT",
366
+ key,
367
+ };
368
+ }
369
+ }
245
370
 
246
- // make children optional
247
- export function Switch({ children = [] }: SwitchProps) {
248
- for (let child of children) {
249
- if (child.props.when) {
250
- return { type: "div", props: {
251
- idKey: crypto.randomUUID()
252
- }, children: [child] };
371
+ if (prevSibling == null) {
372
+ wipFiber.child = newFiber;
373
+ } else {
374
+ prevSibling.sibling = newFiber;
253
375
  }
376
+ prevSibling = newFiber;
254
377
  }
255
- return { type: "div", props: {}, children: [] };
378
+
379
+ oldFibersByKey.forEach(fiber => {
380
+ fiber.effectTag = "DELETION";
381
+ deletions.push(fiber);
382
+ });
383
+
384
+ if (prevSibling) prevSibling.sibling = null;
256
385
  }
257
386
 
258
387
  /**
259
- * @description - Match component
260
- * @param param0
261
- * @returns
388
+ * Creates a virtual DOM element.
389
+ * @param {string|Function} type - The type of the element.
390
+ * @param {object} props - The element's properties.
391
+ * @param {...any} children - The element's children.
392
+ * @returns {VNode} The created virtual DOM element.
262
393
  */
263
- export function Match({ when, children }) {
264
- return when ? children : { type: "div", props: {}, children: [] };
394
+ export function createElement(
395
+ type: string | Function,
396
+ props?: object,
397
+ ...children: any[]
398
+ ): VNode {
399
+ return {
400
+ type,
401
+ props: {
402
+ ...props,
403
+ children: children
404
+ .flat()
405
+ .filter(child => child != null && typeof child !== "boolean")
406
+ .map(child =>
407
+ typeof child === "object" ? child : createTextElement(child)
408
+ ),
409
+ },
410
+ key: props?.key ?? null,
411
+ };
265
412
  }
413
+
266
414
  /**
267
- * @description - Manage state and forceupdate specific affected elements
268
- * @param key
269
- * @param initialState
270
- * @param persist - persist state on reload
271
- * @returns {T, (newState: any, Element: string) => void, key}
415
+ * Creates a text virtual DOM element.
416
+ * @param {string} text - The text content.
417
+ * @returns {VNode} The created text element.
272
418
  */
273
-
274
- export const useState = (initialState, persist) => {
275
- const setState = (newState) => {
276
- initialState = newState;
419
+ function createTextElement(text: string): VNode {
420
+ return {
421
+ type: "TEXT_ELEMENT",
422
+ props: {
423
+ nodeValue: text,
424
+ children: [],
425
+ },
277
426
  };
278
-
279
- return [initialState, setState];
280
- };
427
+ }
281
428
 
282
- if (!isServer) {
283
- window.effects = []
284
- }
285
-
286
-
287
- /**
288
- * @description - Create a new component
289
- * @param element
290
- * @param props
291
- * @param children
292
- * @returns
293
- * @example
294
- * const App = (props) => {
295
- * return (
296
- * <div>
297
- * <h1>Hello, {props.name}</h1>
298
- * </div>
299
- * )
300
- * }
301
- *
302
- * render(<App name="John" />, document.getElementById("root"));
303
- */
304
-
305
- // create a hidden object on window
306
- //
307
- if (!isServer) {
308
- Object.defineProperty(window, "state", {
309
- value: [],
310
- writable: true,
311
- enumerable: true,
312
- })
313
-
314
- } else {
315
- globalThis.state = []
316
- }
317
- export class Component {
318
- props;
319
- state;
320
- element;
321
- Mounted;
322
- effect;
323
- key;
324
- effectCalls: any[]
325
- eventRegistry: any
326
- prevState;
327
- refs: HTMLElement[] | any[]
328
- state: {}
329
- constructor() {
330
- this.key = crypto.randomUUID();
331
- this.props = {};
332
- this.effect = [];
333
- this.Mounted = false;
334
- this.state = {};
335
- this.element = null;
336
- this.effectCalls = []
337
- this.errorThreshold = 1000
338
- this.maxIntervalCalls = 10
339
- this.eventRegistry = new WeakMap();
340
- this.refs = []
341
- }
342
- useRef = (key, value) => {
343
- if (!this.refs.find((r) => r.key == key)) {
344
- this.refs.push({ key, current: value});
345
- }
429
+ /**
430
+ * A React-like useState hook for managing component state.
431
+ * @template T
432
+ * @param {T|(() => T)} initial - The initial state value or initializer function.
433
+ * @returns {[T, (action: T | ((prevState: T) => T)) => void]} A stateful value and a function to update it.
434
+ */
435
+ export function useState<T>(
436
+ initial: T | (() => T)
437
+ ): [T, (action: T | ((prevState: T) => T)) => void] {
438
+ if (!wipFiber) {
439
+ throw new Error("Hooks can only be called inside a Vader.js function component.");
440
+ }
346
441
 
347
- return { key, current: this.refs.find((r) => r.key == key).current };
442
+ let hook = wipFiber.hooks[hookIndex];
443
+ if (!hook) {
444
+ hook = {
445
+ state: typeof initial === "function" ? (initial as () => T)() : initial,
446
+ queue: []
447
+ };
448
+ wipFiber.hooks[hookIndex] = hook;
348
449
  }
349
- useEffect(callback, dependencies = []) {
350
- const callbackId = callback.toString(); // Unique ID based on callback string representation
351
450
 
352
- if (!this.effectCalls.some((effect) => effect.id === callbackId)) {
353
- // Add the initial effect call if it doesn't exist
354
- this.effectCalls.push({
355
- id: callbackId,
356
- count: 0,
357
- lastCall: Date.now(),
358
- hasRun: false, // Tracks if the effect has already run once
359
- dependencies
360
- });
451
+ hook.queue.forEach((action) => {
452
+ hook.state = typeof action === "function" ? action(hook.state) : action;
453
+ });
454
+ hook.queue = [];
455
+
456
+ const setState = (action: T | ((prevState: T) => T)) => {
457
+ hook.queue.push(action);
458
+ if (!isRenderScheduled) {
459
+ isRenderScheduled = true;
460
+ requestAnimationFrame(workLoop);
361
461
  }
462
+ };
362
463
 
363
- const effectCall = this.effectCalls.find((effect) => effect.id === callbackId);
464
+ hookIndex++;
465
+ return [hook.state, setState];
466
+ }
364
467
 
365
- const executeCallback = () => {
366
- const now = Date.now();
367
- const timeSinceLastCall = now - effectCall.lastCall;
368
-
369
- // Track call counts and handle potential over-calling issues
370
- if (timeSinceLastCall < this.errorThreshold) {
371
- effectCall.count += 1;
372
- if (effectCall.count > this.maxIntervalCalls) {
373
- throw new Error(
374
- `Woah, way too many calls! Ensure you are not over-looping. Adjust maxIntervalCalls and errorThreshold as needed.`
375
- );
376
- }
377
- } else {
378
- effectCall.count = 1;
379
- }
468
+ /**
469
+ * A React-like useEffect hook for side effects.
470
+ * @param {Function} callback - The effect callback.
471
+ * @param {Array} deps - The dependency array.
472
+ */
473
+ export function useEffect(callback: Function, deps?: any[]): void {
474
+ if (!wipFiber) {
475
+ throw new Error("Hooks can only be called inside a Vader.js function component.");
476
+ }
477
+
478
+ let hook = wipFiber.hooks[hookIndex];
479
+ if (!hook) {
480
+ hook = { deps: undefined, _cleanupFn: undefined };
481
+ wipFiber.hooks[hookIndex] = hook;
482
+ }
483
+
484
+ const hasChanged = hook.deps === undefined ||
485
+ !deps ||
486
+ deps.some((dep, i) => !Object.is(dep, hook.deps[i]));
380
487
 
381
- effectCall.lastCall = now;
488
+ if (hasChanged) {
489
+ if (hook._cleanupFn) {
490
+ hook._cleanupFn();
491
+ }
492
+ setTimeout(() => {
493
+ const newCleanup = callback();
494
+ if (typeof newCleanup === 'function') {
495
+ hook._cleanupFn = newCleanup;
496
+ } else {
497
+ hook._cleanupFn = undefined;
498
+ }
499
+ }, 0);
500
+ }
501
+
502
+ hook.deps = deps;
503
+ hookIndex++;
504
+ }
382
505
 
383
- setTimeout(() => {
384
- try {
385
- effects.push(callbackId); // Track executed effects
386
- callback(); // Execute the callback
387
- } catch (error) {
388
- console.error(error);
389
- }
390
- }, 0);
391
- };
506
+ /**
507
+ * A switch component for conditional rendering.
508
+ * @param {object} props - The component props.
509
+ * @param {VNode[]} props.children - The child components.
510
+ * @returns {VNode|null} The matched child or null.
511
+ */
512
+ export function Switch({ children }: { children: VNode[] }): VNode | null {
513
+ const childrenArray = Array.isArray(children) ? children : [children];
514
+ const match = childrenArray.find(child => child && child.props.when);
515
+ if (match) {
516
+ return match;
517
+ }
518
+ return childrenArray.find(child => child && child.props.default) || null;
519
+ }
392
520
 
393
- // First time: Run the effect and mark it as run
394
- if (!effectCall.hasRun && dependencies.length === 0) {
395
- executeCallback();
396
- effectCall.hasRun = true;
397
- effectCall.dependencies = dependencies;
398
- return;
399
- }
521
+ /**
522
+ * A match component for use with Switch.
523
+ * @param {object} props - The component props.
524
+ * @param {boolean} props.when - The condition to match.
525
+ * @param {VNode[]} props.children - The child components.
526
+ * @returns {VNode|null} The children if when is true, otherwise null.
527
+ */
528
+ export function Match({ when, children }: { when: boolean, children: VNode[] }): VNode | null {
529
+ return when ? children : null;
530
+ }
400
531
 
401
- // If there are no dependencies, do nothing after the first run
402
- if (dependencies.length === 0) {
403
- return;
404
- }
532
+ /**
533
+ * A React-like useRef hook for mutable references.
534
+ * @template T
535
+ * @param {T} initial - The initial reference value.
536
+ * @returns {{current: T}} A mutable ref object.
537
+ */
538
+ export function useRef<T>(initial: T): { current: T } {
539
+ if (!wipFiber) {
540
+ throw new Error("Hooks can only be called inside a Vader.js function component.");
541
+ }
405
542
 
406
- // Check if dependencies have changed
407
- let dependenciesChanged = false;
408
- for (let i = 0; i < dependencies.length; i++) {
409
- const previousDependencies = effectCall.dependencies || [];
410
- if (
411
- JSON.stringify(previousDependencies[i]) !== JSON.stringify(dependencies[i])
412
- ) {
413
- dependenciesChanged = true;
414
- break;
415
- }
416
- }
543
+ let hook = wipFiber.hooks[hookIndex];
544
+ if (!hook) {
545
+ hook = { current: initial };
546
+ wipFiber.hooks[hookIndex] = hook;
547
+ }
417
548
 
418
- // If dependencies have changed, run the effect and update dependencies
419
- if (dependenciesChanged) {
420
- executeCallback();
421
- effectCall.dependencies = dependencies;
422
- }
549
+ hookIndex++;
550
+ return hook;
423
551
  }
424
552
 
425
-
426
- useState(key, defaultValue, persist = false) {
427
- let value = this.state[key] || defaultValue;
428
- if(value === "true" || value === "false") {
429
- value = JSON.parse(value);
430
- }
431
- // if value is boolean store as string
553
+ /**
554
+ * A React-like useLayoutEffect hook that runs synchronously after DOM mutations.
555
+ * @param {Function} callback - The effect callback.
556
+ * @param {Array} deps - The dependency array.
557
+ */
558
+ export function useLayoutEffect(callback: Function, deps?: any[]): void {
559
+ if (!wipFiber) {
560
+ throw new Error("Hooks can only be called inside a Vader.js function component.");
561
+ }
432
562
 
433
- if (persist) {
434
- value = sessionStorage.getItem(key) ? JSON.parse(sessionStorage.getItem(key)).value : defaultValue;
563
+ let hook = wipFiber.hooks[hookIndex];
564
+ if (!hook) {
565
+ hook = { deps: undefined, _cleanupFn: undefined };
566
+ wipFiber.hooks[hookIndex] = hook;
567
+ }
568
+
569
+ const hasChanged = hook.deps === undefined ||
570
+ !deps ||
571
+ deps.some((dep, i) => !Object.is(dep, hook.deps[i]));
572
+
573
+ if (hasChanged) {
574
+ if (hook._cleanupFn) {
575
+ hook._cleanupFn();
435
576
  }
436
- const setValue = (newValue) => {
437
- if(typeof newValue === "function") {
438
- newValue = newValue(this.state[key]);
439
- }
440
- this.state[key] = typeof newValue === "boolean" ? newValue.toString() : newValue;
441
- if (persist) {
442
- sessionStorage.setItem(key, JSON.stringify({ value: newValue }));
443
- }
444
- this.forceUpdate(this.key);
445
- };
446
- return [value, setValue];
447
- }
448
- useFetch(url, options) {
449
- const loadingKey = "loading_" + url;
450
- const errorKey = "error" + url;
451
- const dataKey = "_data" + url;
452
- let [loading, setLoading, _clear1] = this.useState(loadingKey, true);
453
- let [error, setError, _clear2] = this.useState(errorKey, null);
454
- let [data, setData, clear] = this.useState(dataKey, null);
455
- if (loading() && !error() && !data()) {
456
- fetch(url, options).then((res) => res.json()).then((data2) => {
457
- setLoading(false);
458
- setData(data2);
459
- this.forceUpdate(this.key);
460
- setTimeout(() => {
461
- _clear1()
462
- _clear2()
463
- clear()
464
- }, 1500)
465
- }).catch((err) => {
466
- setError(err);
467
- this.forceUpdate(this.key);
468
- });
577
+ const cleanup = callback();
578
+ if (typeof cleanup === 'function') {
579
+ hook._cleanupFn = cleanup;
580
+ } else {
581
+ hook._cleanupFn = undefined;
469
582
  }
470
- return { loading, error, data };
471
583
  }
472
- addEventListener(element, event, handler) {
473
- if (!this.eventRegistry.has(element)) {
474
- this.eventRegistry.set(element, []);
584
+
585
+ hook.deps = deps;
586
+ hookIndex++;
587
+ }
588
+
589
+ /**
590
+ * A React-like useReducer hook for state management with reducers.
591
+ * @template S
592
+ * @template A
593
+ * @param {(state: S, action: A) => S} reducer - The reducer function.
594
+ * @param {S} initialState - The initial state.
595
+ * @returns {[S, (action: A) => void]} The current state and dispatch function.
596
+ */
597
+ export function useReducer<S, A>(
598
+ reducer: (state: S, action: A) => S,
599
+ initialState: S
600
+ ): [S, (action: A) => void] {
601
+ if (!wipFiber) {
602
+ throw new Error("Hooks can only be called inside a Vader.js function component.");
603
+ }
604
+
605
+ let hook = wipFiber.hooks[hookIndex];
606
+ if (!hook) {
607
+ hook = {
608
+ state: initialState,
609
+ queue: [],
610
+ };
611
+ wipFiber.hooks[hookIndex] = hook;
612
+ }
613
+
614
+ hook.queue.forEach((action) => {
615
+ hook.state = reducer(hook.state, action);
616
+ });
617
+ hook.queue = [];
618
+
619
+ const dispatch = (action: A) => {
620
+ hook.queue.push(action);
621
+ if (!isRenderScheduled) {
622
+ isRenderScheduled = true;
623
+ requestAnimationFrame(workLoop);
475
624
  }
476
- const registeredEvents = this.eventRegistry.get(element);
477
- const isDuplicate = registeredEvents.some((e) => e.type === event && e.handler === handler);
478
- if (!isDuplicate) {
479
- element["on" + event] = handler;
480
- registeredEvents.push({ type: event, handler });
481
- this.eventRegistry.set(element, registeredEvents);
625
+ };
626
+
627
+ hookIndex++;
628
+ return [hook.state, dispatch];
629
+ }
630
+
631
+ /**
632
+ * A React-like useContext hook for accessing context values.
633
+ * @template T
634
+ * @param {Context<T>} Context - The context object to use.
635
+ * @returns {T} The current context value.
636
+ */
637
+ export function useContext<T>(Context: Context<T>): T {
638
+ if (!wipFiber) {
639
+ throw new Error("Hooks can only be called inside a Vader.js function component.");
640
+ }
641
+
642
+ let fiber = wipFiber.parent;
643
+ while (fiber) {
644
+ if (fiber.type && fiber.type._context === Context) {
645
+ return fiber.props.value;
482
646
  }
647
+ fiber = fiber.parent;
483
648
  }
484
- removeEventListeners(element) {
485
- // Unregister and remove all events for the element
486
- const registeredEvents = this.eventRegistry.get(element) || [];
487
- registeredEvents.forEach(({ type, handler }) => {
488
- element.removeEventListener(type, handler);
489
- });
490
- this.eventRegistry.delete(element);
491
- }
492
- forceUpdate(key) {
493
- let el = document.querySelector(`[idKey="${key}"]`);
494
- let newl = this.toElement(this.props);
495
- if (newl.getAttribute("idKey") !== key) {
496
- newl = Array.from(newl.children).filter((el2) => el2.getAttribute("idKey") === key)[0];
497
- }
498
- this.Reconciler.update(el, newl);
499
- }
500
- attachEventsRecursively = (element, source) => {
501
- // Rebind events for the current element
502
- const events = this.eventRegistry.get(source) || [];
503
- events.forEach(({ event, handler }) => {
504
- this.addEventListener(element, event, handler);
505
- });
506
-
507
- // Traverse children recursively
508
- const children = Array.from(source.childNodes || []);
509
- const elementChildren = Array.from(element.childNodes || []);
510
-
511
- children.forEach((child, index) => {
512
- if (elementChildren[index]) {
513
- this.attachEventsRecursively(elementChildren[index], child);
514
- }
515
- });
649
+
650
+ return Context._defaultValue;
651
+ }
652
+
653
+ interface Context<T> {
654
+ _defaultValue: T;
655
+ Provider: Function & { _context: Context<T> };
656
+ }
657
+
658
+ /**
659
+ * Creates a context object for use with useContext.
660
+ * @template T
661
+ * @param {T} defaultValue - The default context value.
662
+ * @returns {Context<T>} The created context object.
663
+ */
664
+ export function createContext<T>(defaultValue: T): Context<T> {
665
+ const context = {
666
+ _defaultValue: defaultValue,
667
+ Provider: function Provider({ children }: { children: VNode[] }) {
668
+ return children;
669
+ },
516
670
  };
671
+ context.Provider._context = context;
672
+ return context;
673
+ }
517
674
 
675
+ /**
676
+ * A React-like useMemo hook for memoizing expensive calculations.
677
+ * @template T
678
+ * @param {() => T} factory - The function to memoize.
679
+ * @param {Array} deps - The dependency array.
680
+ * @returns {T} The memoized value.
681
+ */
682
+ export function useMemo<T>(factory: () => T, deps?: any[]): T {
683
+ if (!wipFiber) {
684
+ throw new Error("Hooks can only be called inside a Vader.js function component.");
685
+ }
518
686
 
519
- Reconciler = {
520
- update: (oldElement, newElement) => {
521
- if (!oldElement || !newElement) return;
522
-
523
- // Check if the current element needs an update
524
- if (this.Reconciler.shouldUpdate(oldElement, newElement)) {
525
- // Update attributes
526
- const oldChildren = Array.from(oldElement.childNodes);
527
- const newChildren = Array.from(newElement.childNodes);
528
-
529
- const maxLength = Math.max(oldChildren.length, newChildren.length);
530
- if (oldElement.tagName !== newElement.tagName) {
531
- const newElementClone = newElement.cloneNode(true);
532
- oldElement.replaceWith(newElementClone);
533
-
534
- // Attach events recursively to the new element
535
- this.attachEventsRecursively(newElementClone, newElement);
536
- return;
537
- }
687
+ let hook = wipFiber.hooks[hookIndex];
688
+ if (!hook) {
689
+ hook = { memoizedValue: factory(), deps };
690
+ wipFiber.hooks[hookIndex] = hook;
691
+ }
538
692
 
539
- for (let i = 0; i < maxLength; i++) {
540
- if (i >= oldChildren.length) {
541
- const newChildClone = newChildren[i].cloneNode(true);
542
- if(oldElement.nodeType === Node.TEXT_NODE) {
543
- oldElement.textContent = newElement.textContent;
544
- return;
545
- }
546
- oldElement.appendChild(newChildClone);
547
-
548
- // Rebind events to the new child (and its children recursively)
549
- this.attachEventsRecursively(newChildClone, newChildren[i]);
550
- } else if (i >= newChildren.length) {
551
- oldElement.removeChild(oldChildren[i]);
552
- } else {
553
- this.Reconciler.update(oldChildren[i], newChildren[i]);
554
- }
555
- }
556
-
557
- Array.from(oldElement.attributes || []).forEach(({ name }) => {
558
- if (!newElement.hasAttribute(name)) {
559
- oldElement.removeAttribute(name);
560
- }
561
- });
562
-
563
- Array.from(newElement.attributes || []).forEach(({ name, value }) => {
564
- if (oldElement.getAttribute(name) !== value) {
565
- oldElement.setAttribute(name, value);
566
- }
567
- });
568
-
569
- // Handle text node updates
570
- if (oldElement.nodeType === Node.TEXT_NODE) {
571
- if (oldElement.textContent !== newElement.textContent) {
572
- oldElement.textContent = newElement.textContent;
573
- }
574
- return;
575
- }
576
-
577
- // If the element has a single text node, update text directly
578
- if (
579
- oldElement.childNodes.length === 1 &&
580
- oldElement.firstChild.nodeType === Node.TEXT_NODE
581
- ) {
582
- if (oldElement.textContent !== newElement.textContent) {
583
- oldElement.textContent = newElement.textContent;
584
- }
693
+ const hasChanged = hook.deps === undefined ||
694
+ !deps ||
695
+ deps.some((dep, i) => !Object.is(dep, hook.deps[i]));
696
+ if (hasChanged) {
697
+ hook.memoizedValue = factory();
698
+ hook.deps = deps;
699
+ }
700
+
701
+ hookIndex++;
702
+ return hook.memoizedValue;
703
+ }
704
+
705
+ /**
706
+ * A React-like useCallback hook for memoizing functions.
707
+ * @template T
708
+ * @param {T} callback - The function to memoize.
709
+ * @param {Array} deps - The dependency array.
710
+ * @returns {T} The memoized callback.
711
+ */
712
+ export function useCallback<T extends Function>(callback: T, deps?: any[]): T {
713
+ return useMemo(() => callback, deps);
714
+ }
715
+
716
+ /**
717
+ * A hook for managing arrays with common operations.
718
+ * @template T
719
+ * @param {T[]} initialValue - The initial array value.
720
+ * @returns {{
721
+ * array: T[],
722
+ * add: (item: T) => void,
723
+ * remove: (index: number) => void,
724
+ * update: (index: number, item: T) => void
725
+ * }} An object with the array and mutation functions.
726
+ */
727
+ export function useArray<T>(initialValue: T[] = []): {
728
+ array: T[],
729
+ add: (item: T) => void,
730
+ remove: (index: number) => void,
731
+ update: (index: number, item: T) => void
732
+ } {
733
+ const [array, setArray] = useState(initialValue);
734
+
735
+ const add = (item: T) => {
736
+ setArray((prevArray) => [...prevArray, item]);
737
+ };
738
+
739
+ const remove = (index: number) => {
740
+ setArray((prevArray) => prevArray.filter((_, i) => i !== index));
741
+ };
742
+
743
+ const update = (index: number, item: T) => {
744
+ setArray((prevArray) => prevArray.map((prevItem, i) => (i === index ? item : prevItem)));
745
+ };
746
+
747
+ return { array, add, remove, update };
748
+ }
749
+
750
+ /**
751
+ * A hook for running a function at a fixed interval.
752
+ * @param {Function} callback - The function to run.
753
+ * @param {number|null} delay - The delay in milliseconds, or null to stop.
754
+ */
755
+ export function useInterval(callback: Function, delay: number | null): void {
756
+ useEffect(() => {
757
+ if (delay === null) return;
758
+ const interval = setInterval(callback, delay);
759
+ return () => clearInterval(interval);
760
+ }, [callback, delay]);
761
+ }
762
+
763
+ // Types for cache configuration
764
+ interface QueryCacheOptions {
765
+ expiryMs?: number; // Cache duration in milliseconds
766
+ enabled?: boolean; // Whether caching is enabled
767
+ }
768
+
769
+ // Default cache options
770
+ const DEFAULT_CACHE_OPTIONS: QueryCacheOptions = {
771
+ expiryMs: 5 * 60 * 1000, // 5 minutes default
772
+ enabled: true
773
+ };
774
+
775
+ // In-memory cache store
776
+ const queryCache = new Map<string, {
777
+ data: any;
778
+ timestamp: number;
779
+ options: QueryCacheOptions;
780
+ }>();
781
+
782
+ export function useQuery<T>(
783
+ url: string,
784
+ cacheOptions: QueryCacheOptions = {} // Default to empty object
785
+ ): {
786
+ data: T | null;
787
+ loading: boolean;
788
+ error: Error | null;
789
+ refetch: () => Promise<void>;
790
+ } {
791
+ const [data, setData] = useState<T | null>(null);
792
+ const [loading, setLoading] = useState(true);
793
+ const [error, setError] = useState<Error | null>(null);
794
+
795
+ // FIX: Destructure primitive values from cacheOptions for stable dependencies.
796
+ const {
797
+ enabled = DEFAULT_CACHE_OPTIONS.enabled,
798
+ expiryMs = DEFAULT_CACHE_OPTIONS.expiryMs
799
+ } = cacheOptions;
800
+
801
+ // FIX: Memoize the options object so its reference is stable across renders.
802
+ // It will only be recreated if `enabled` or `expiryMs` changes.
803
+ const mergedCacheOptions = useMemo(() => ({
804
+ enabled,
805
+ expiryMs,
806
+ }), [enabled, expiryMs]);
807
+
808
+ const fetchData = useCallback(async () => {
809
+ setLoading(true);
810
+ try {
811
+ // Check cache first if enabled
812
+ if (mergedCacheOptions.enabled) {
813
+ const cached = queryCache.get(url);
814
+ const now = Date.now();
815
+
816
+ if (cached && now - cached.timestamp < mergedCacheOptions.expiryMs) {
817
+ setData(cached.data);
818
+ setLoading(false);
585
819
  return;
586
820
  }
587
821
  }
588
-
589
- // Process children recursively
590
- const oldChildren = Array.from(oldElement.childNodes);
591
- const newChildren = Array.from(newElement.childNodes);
592
-
593
- const maxLength = Math.max(oldChildren.length, newChildren.length);
594
-
595
- for (let i = 0; i < maxLength; i++) {
596
- if (i >= oldChildren.length) {
597
- // Add new child if it exists in newChildren but not in oldChildren
598
- const newChildClone = newChildren[i].cloneNode(true);
599
- oldElement.appendChild(newChildClone);
600
-
601
- // Attach any event listeners
602
- const newChildEvents = this.eventRegistry.get(newChildren[i]) || [];
603
- newChildEvents.forEach(({ type, handler }) => {
604
- this.addEventListener(newChildClone, type, handler);
605
- });
606
- } else if (i >= newChildren.length) {
607
- // Remove child if it exists in oldChildren but not in newChildren
608
- oldElement.removeChild(oldChildren[i]);
609
- } else {
610
- this.Reconciler.update(oldChildren[i], newChildren[i]);
611
- }
612
- }
613
-
614
- // Reapply events for the current element
615
- const parentEvents = this.eventRegistry.get(newElement) || [];
616
- parentEvents.forEach(({ type, handler }) => {
617
- if (newElement.nodeType === oldElement.nodeType) {
618
- this.addEventListener(oldElement, type, handler);
619
- }
620
- });
621
822
 
622
- },
623
- shouldUpdate: (oldElement, newElement) => {
624
- // Check if node types differ
625
- if (oldElement.nodeType !== newElement.nodeType) {
626
- return true;
627
- }
628
-
629
- // Check if text content differs
630
- if (oldElement.nodeType === Node.TEXT_NODE) {
631
- return oldElement.textContent !== newElement.textContent;
632
- }
633
-
634
- // Check if node names differ
635
- if (oldElement.nodeName !== newElement.nodeName) {
636
- return true;
637
- }
638
-
639
- // Check if child counts differ
640
- if (oldElement.childNodes.length !== newElement.childNodes.length) {
641
- return true;
642
- }
643
-
644
- // Check if attributes differ
645
- const newAttributes = Array.from(newElement.attributes || []);
646
- for (let { name, value } of newAttributes) {
647
- if (oldElement.getAttribute(name) !== value) {
648
- return true;
649
- }
823
+ // Not in cache or expired - fetch fresh data
824
+ const response = await fetch(url);
825
+
826
+ if (!response.ok) {
827
+ throw new Error(`HTTP error! status: ${response.status}`);
650
828
  }
651
-
652
- // If no differences found, no update needed
653
- return false;
654
- },
655
- }
656
-
657
- parseToElement = (element) => {
658
- if (!element || element.nodeType) return ""
659
-
660
- let svgTags = ["svg", "path", "circle", "rect", "line", "polyline", "polygon", "ellipse", "g"];
661
- let isSvg = svgTags.includes(element.type);
662
-
663
- // Create the element, using proper namespace for SVG
664
- let el = isSvg
665
- ? document.createElementNS("http://www.w3.org/2000/svg", element.type)
666
- : document.createElement(element.type);
667
-
668
- // Handle text nodes
669
- if (typeof element === "string" || typeof element === "number" || typeof element === "boolean") {
670
- el.textContent = element; // Safer alternative to innerHTML
671
- return el;
672
- }
673
-
674
- // Set attributes
675
- let attributes = element.props || {};
676
- for (let key in attributes) {
677
- if(typeof attributes[key] !== "string" && !acceptedAttributes.includes(key) || !acceptedAttributes.includes(key)) continue;
678
- if(key === "ref") {
679
- let _key = attributes[key].key;
680
- // update the ref
681
- let ref = this.refs.find((r) => r.key == _key);
682
- if(ref) {
683
- ref.current = document.querySelector(`[idKey="${_key}"]`) || el;
684
- }
685
- el.setAttribute("idKey", _key);
686
- element.props.idKey = _key
687
- }
688
- else if (key === "key") {
689
- el.key = attributes[key];
690
- } else if (key === "className") {
691
- el.setAttribute("class", attributes[key]);
692
- } else if (key === "style") {
693
- let styleObject = attributes[key];
694
- if (typeof styleObject === "object") {
695
- for (let styleKey in styleObject) {
696
- el.style[styleKey] = styleObject[styleKey];
697
- }
698
- }
699
- } else if (key.startsWith("on")) {
700
- // Event listeners
701
- const eventType = key.substring(2).toLowerCase();
702
- const handler = attributes[key];
703
- this.eventRegistry.set(el, [...(this.eventRegistry.get(el) || []), { event: eventType, handler }]);
704
- this.addEventListener(el, eventType, handler);
705
- } else if (attributes[key] !== null && attributes[key] !== undefined &&
706
- !key.includes(" ") || !key.includes("-") || !key.includes("_")) {
707
- try {
708
- el.setAttribute(key, attributes[key]);
709
- } catch (error) {
710
-
711
- }
712
- } else if(typeof attributes[key] === "object" && key !== "style"){
713
- continue;
714
- }
715
- }
716
-
717
- // Handle children
718
- let children = element.children || [];
719
- children.forEach((child) => {
720
- if (Array.isArray(child)) {
721
- // Recursively process nested arrays
722
- child.forEach((nestedChild) => el.appendChild(this.parseToElement(nestedChild)));
723
- } else if (typeof child === "function") {
724
- // Handle functional components
725
- let component = memoizeClassComponent(Component, child.name);
726
- component.Mounted = true;
727
- component.render = (props) => child(props);
728
- let componentElement = component.toElement();
729
- el.appendChild(componentElement);
730
- } else if (typeof child === "object") {
731
- // Nested object children
732
- el.appendChild(this.parseToElement(child));
733
- } else if (child !== null && child !== undefined && child !== false) {
734
- // Text nodes
735
- el.appendChild(document.createTextNode(child));
829
+
830
+ const result = await response.json();
831
+
832
+ // Update cache if enabled
833
+ if (mergedCacheOptions.enabled) {
834
+ queryCache.set(url, {
835
+ data: result,
836
+ timestamp: Date.now(),
837
+ options: mergedCacheOptions
838
+ });
736
839
  }
737
- });
738
-
739
- return el;
740
- };
741
- e(element, props, ...children) {
742
- if (typeof element === "function") {
743
- return element();
840
+
841
+ setData(result);
842
+ } catch (err) {
843
+ setError(err instanceof Error ? err : new Error(String(err)));
844
+ } finally {
845
+ setLoading(false);
744
846
  }
745
- return { type: element, props: props || {}, children: children || [] };
746
- }
747
- toElement() {
748
- let children = this.render(this.props);
749
- let el = this.parseToElement(children);
750
- el.setAttribute("idKey", this.key);
751
- return el;
752
- }
753
- render() {
754
- return "";
755
- }
756
- }
847
+ }, [url, mergedCacheOptions]); // This dependency is now stable
757
848
 
758
- function memoizeClassComponent(Component, key) {
759
- let instance = memoizes.get(key);
760
- if (!instance) {
761
- instance = new Component(key);
762
- memoizes.set(key, instance);
763
- }
764
- return instance;
849
+ useEffect(() => {
850
+ fetchData();
851
+ }, [fetchData]); // This dependency is now stable
852
+
853
+ return { data, loading, error, refetch: fetchData };
765
854
  }
855
+
766
856
  /**
767
- * @description - Render jsx Componenet to the DOM
768
- * @param element
769
- * @param container
857
+ * A hook for tracking window focus state.
858
+ * @returns {boolean} True if the window is focused.
770
859
  */
771
- export function render(element, container) {
772
- // CLEAR STATE ON RELOAD
773
- if (!isServer) {
774
- window.addEventListener("beforeunload", () => {
775
- let keys = Object.keys(sessionStorage);
776
- keys.forEach((key) => {
777
- if (key.startsWith("state_")) {
778
- sessionStorage.removeItem(key);
779
- }
780
- });
781
- });
782
- }
783
- if (isClassComponent(element)) {
784
- const instance = new element;
785
- instance.Mounted = true;
786
- let el = instance.toElement();
787
- instance.element = el;
788
- container.innerHTML = "";
789
- container.replaceWith(el);
790
- } else {
791
- let memoizedInstance = memoizeClassComponent(Component, element.name);
792
- memoizedInstance.Mounted = true;
793
- element = element.bind(memoizedInstance);
860
+ export function useWindowFocus(): boolean {
861
+ const [isFocused, setIsFocused] = useState(true);
862
+
863
+ useEffect(() => {
864
+ const onFocus = () => setIsFocused(true);
865
+ const onBlur = () => setIsFocused(false);
794
866
 
795
- memoizedInstance.render = (props) => element(props);
796
- if (element.name == "default") {
797
- throw new Error("Function name Must be a unique function name as it is used for a element key");
867
+ window.addEventListener("focus", onFocus);
868
+ window.addEventListener("blur", onBlur);
869
+
870
+ return () => {
871
+ window.removeEventListener("focus", onFocus);
872
+ window.removeEventListener("blur", onBlur);
873
+ };
874
+ }, []);
875
+
876
+ return isFocused;
877
+ }
878
+
879
+ /**
880
+ * A hook for syncing state with localStorage.
881
+ * @template T
882
+ * @param {string} key - The localStorage key.
883
+ * @param {T} initialValue - The initial value.
884
+ * @returns {[T, (value: T) => void]} The stored value and a function to update it.
885
+ */
886
+ export function useLocalStorage<T>(key: string, initialValue: T): [T, (value: T) => void] {
887
+ const [storedValue, setStoredValue] = useState(() => {
888
+ try {
889
+ const item = localStorage.getItem(key);
890
+ return item ? JSON.parse(item) : initialValue;
891
+ } catch (error) {
892
+ return initialValue;
798
893
  }
799
- memoizedInstance.key = element.name;
800
- let el = memoizedInstance.toElement();
801
- el.key = element.name;
802
- container.innerHTML = "";
803
- container.replaceWith(el);
804
- }
894
+ });
895
+
896
+ const setValue = (value: T) => {
897
+ try {
898
+ setStoredValue(value);
899
+ localStorage.setItem(key, JSON.stringify(value));
900
+ } catch (error) {
901
+ console.error("Error saving to localStorage", error);
902
+ }
903
+ };
904
+
905
+ return [storedValue, setValue];
906
+ }
907
+
908
+ /**
909
+ * A hook for detecting clicks outside an element.
910
+ * @param {React.RefObject} ref - A ref to the element to watch.
911
+ * @param {Function} handler - The handler to call when a click outside occurs.
912
+ */
913
+ export function useOnClickOutside(ref: { current: HTMLElement | null }, handler: Function): void {
914
+ useEffect(() => {
915
+ const listener = (event: MouseEvent) => {
916
+ if (ref.current && !ref.current.contains(event.target as Node)) {
917
+ handler(event);
918
+ }
919
+ };
920
+ document.addEventListener("mousedown", listener);
921
+ return () => {
922
+ document.removeEventListener("mousedown", listener);
923
+ };
924
+ }, [ref, handler]);
805
925
  }