vaderjs 2.2.6 → 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,774 +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");
61
57
 
62
58
  /**
63
- * @description - useRef allows you to store a reference to a DOM element
64
- * @param value
65
- * @returns {current: HTMLElement}
66
- * @example
67
- * const inputRef = useRef();
68
- * <input ref={inputRef} />
69
- * console.log(inputRef.current) // <input />
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.
70
62
  */
71
- export const useRef = (value) => {
72
- return { key: crypto.randomUUID(), current: value };
73
- }
63
+ const isProperty = (key: string) => key !== "children" && !isEvent(key);
74
64
 
75
65
  /**
76
- * @description - Handle asyncronous promises and return the data or error;
77
- * @param promise
78
- * @returns
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.
79
70
  */
80
- export const useAsyncState = (promise: Promise<any>) => {
81
- return [null, () => { }];
82
- }
83
- export const useEffect = (callback: any, dependencies: any[] = []) => {
84
- dependencies = dependencies.map((dep) => JSON.stringify(dep));
85
- if (dependencies.length === 0) {
86
- callback();
87
- }
88
- }
89
-
90
- // make a switch function component
71
+ const isNew = (prev: object, next: object) => (key: string) => prev[key] !== next[key];
91
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);
92
80
 
93
- export const A = (props: {
94
- /**
95
- * @description Set the elements classlist
96
- */
97
- className?: string;
98
- /**
99
- * @description Once clicked send user to a different link
100
- */
101
- href?: string;
102
- style?: string;
103
- openInNewTab?: boolean
104
- onClick?: () => void;
105
- onChange?: () => void;
106
- }, children: any) => {
107
- function handleClick(e) {
108
- e.preventDefault();
109
- if (props.openInNewTab) {
110
- window.open(props.href, "_blank");
111
- return void 0;
112
- }
113
- window.history.pushState({}, "", props.href);
114
- window.dispatchEvent(new Event("popstate"));
115
- window.dispatchEvent(new Event("load"));
116
- window.location.reload();
117
- return void 0;
118
- }
119
- return e("a", { ...props, onClick: handleClick }, props.children);
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;
120
94
  }
121
95
 
122
-
123
- export const Fragment = (props: any, children: any) => {
124
- return {
125
- type: null,
126
- props: props,
127
- children
128
- }
129
- }
130
-
131
- if(typeof window !== "undefined") {
132
- window.history.back = () => {
133
- window.history.go(-1);
134
- }
135
- window.history.forward = () => {
136
- window.history.go(1);
137
- }
138
- }
139
- globalThis.Fragment = Fragment;
140
-
141
- /**
142
- * @description - Create a new element
143
- * @param element
144
- * @param props
145
- * @param children
146
- * @returns
147
- */
148
- export const e = (element, props, ...children) => {
149
- if (!element)
150
- return "";
151
- let instance;
152
- switch (true) {
153
- case isClassComponent(element):
154
- instance = new element;
155
- instance.props = props;
156
- instance.children = children;
157
- instance.Mounted = true;
158
- return instance.render(props);
159
- case typeof element === "function":
160
- instance = memoizeClassComponent(Component, element.name);
161
- element = element.bind(instance);
162
- instance.render = (props) => element(props);
163
- if (element.name.toLowerCase() == "default") {
164
- throw new Error("Function name must be unique");
96
+ /**
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.
101
+ */
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]);
165
114
  }
166
- instance.key = element.name;
167
- instance.Mounted = true;
168
- let firstEl = instance.render({ key: instance.key, children, ...props }, children);
169
- instance.children = children;
170
- if (!firstEl)
171
- firstEl = { type: "div", props: { key: instance.key, ...props }, children };
172
- firstEl.props = { key: instance.key, ...firstEl.props, ...props };
173
- firstEl.props["idKey"] = instance.props?.ref?.key || instance.key;
174
- instance.props = firstEl.props;
175
- return firstEl;
176
- default:
177
- if(!element) {
178
- return "";
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] = "";
179
127
  }
180
- let el = { type: element, props: props || {}, children: children || [] };
181
- if (el.type !== "head") {
182
- el.props = { idKey: el.props?.ref?.key || crypto.randomUUID(), ...el.props };
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];
183
142
  }
143
+ });
184
144
 
185
- // if element == false return empty string
186
- if (el.type === false) {
187
- return "";
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);
188
154
  }
155
+ });
156
+ }
189
157
 
190
- return el;
191
- }
192
- };
158
+ /**
159
+ * Commits the entire work-in-progress tree to the DOM.
160
+ */
161
+ function commitRoot(): void {
162
+ deletions.forEach(commitWork);
163
+ commitWork(wipRoot.child);
164
+ currentRoot = wipRoot;
165
+ wipRoot = null;
166
+ isRenderScheduled = false;
167
+ }
193
168
 
194
- /*
195
- * @description - Switch component
196
- * @param element
197
- * @param props
198
- * @param children
199
- * @returns
200
- */
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
+ }
201
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
+ }
202
191
 
203
- interface SwitchProps {
204
- children: any[] | any;
192
+ commitWork(fiber.child);
193
+ commitWork(fiber.sibling);
205
194
  }
206
195
 
207
- // make children optional
208
- export function Switch({ children = [] }: SwitchProps) {
209
- for (let child of children) {
210
- if (child.props.when) {
211
- return { type: "div", props: {
212
- idKey: crypto.randomUUID()
213
- }, children: [child] };
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);
214
207
  }
208
+ } else if (fiber.child) {
209
+ commitDeletion(fiber.child);
215
210
  }
216
- return { type: "div", props: {}, children: [] };
217
211
  }
218
212
 
219
213
  /**
220
- * @description - Match component
221
- * @param param0
222
- * @returns
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.
223
217
  */
224
- export function Match({ when, children }) {
225
- return when ? children : { type: "div", props: {}, children: [] };
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);
234
+ }
226
235
  }
236
+
227
237
  /**
228
- * @description - Manage state and forceupdate specific affected elements
229
- * @param key
230
- * @param initialState
231
- * @param persist - persist state on reload
232
- * @returns {T, (newState: any, Element: string) => void, key}
238
+ * The main work loop for rendering and reconciliation.
233
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
+ }
234
250
 
235
- export const useState = (initialState, persist) => {
236
- const setState = (newState) => {
237
- initialState = newState;
238
- };
239
-
240
- return [initialState, setState];
241
- };
251
+ while (nextUnitOfWork) {
252
+ nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
253
+ }
242
254
 
243
- if (!isServer) {
244
- window.effects = []
245
- }
246
-
247
-
248
- /**
249
- * @description - Create a new component
250
- * @param element
251
- * @param props
252
- * @param children
253
- * @returns
254
- * @example
255
- * const App = (props) => {
256
- * return (
257
- * <div>
258
- * <h1>Hello, {props.name}</h1>
259
- * </div>
260
- * )
261
- * }
262
- *
263
- * render(<App name="John" />, document.getElementById("root"));
264
- */
265
-
266
- // create a hidden object on window
267
- //
268
- if (!isServer) {
269
- Object.defineProperty(window, "state", {
270
- value: [],
271
- writable: true,
272
- enumerable: true,
273
- })
274
-
275
- } else {
276
- globalThis.state = []
277
- }
278
- if (!crypto.randomUUID) {
279
- crypto.randomUUID = function() {
280
- const url = URL.createObjectURL(new Blob());
281
- const uuid = url.toString();
282
- URL.revokeObjectURL(url);
283
- return uuid.slice(uuid.lastIndexOf('/') + 1);
284
- };
255
+ if (!nextUnitOfWork && wipRoot) {
256
+ commitRoot();
257
+ }
285
258
  }
286
259
 
287
- export class Component {
288
- props;
289
- state;
290
- element;
291
- Mounted;
292
- effect;
293
- key;
294
- effectCalls: any[]
295
- eventRegistry: any
296
- prevState;
297
- refs: HTMLElement[] | any[]
298
- state: {}
299
- constructor() {
300
- this.key = crypto.randomUUID();
301
- this.props = {};
302
- this.effect = [];
303
- this.Mounted = false;
304
- this.state = {};
305
- this.element = null;
306
- this.effectCalls = []
307
- this.errorThreshold = 1000
308
- this.maxIntervalCalls = 10
309
- this.eventRegistry = new WeakMap();
310
- this.refs = []
311
- }
312
- useRef = (key, value) => {
313
- if (!this.refs.find((r) => r.key == key)) {
314
- this.refs.push({ key, current: value});
315
- }
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;
316
267
 
317
- return { key, current: this.refs.find((r) => r.key == key).current };
268
+ if (isFunctionComponent) {
269
+ updateFunctionComponent(fiber);
270
+ } else {
271
+ updateHostComponent(fiber);
318
272
  }
319
- useEffect(callback, dependencies = []) {
320
- const callbackId = callback.toString(); // Unique ID based on callback string representation
321
273
 
322
- if (!this.effectCalls.some((effect) => effect.id === callbackId)) {
323
- // Add the initial effect call if it doesn't exist
324
- this.effectCalls.push({
325
- id: callbackId,
326
- count: 0,
327
- lastCall: Date.now(),
328
- hasRun: false, // Tracks if the effect has already run once
329
- dependencies
330
- });
274
+ if (fiber.child) {
275
+ return fiber.child;
276
+ }
277
+
278
+ let nextFiber = fiber;
279
+ while (nextFiber) {
280
+ if (nextFiber.sibling) {
281
+ return nextFiber.sibling;
331
282
  }
283
+ nextFiber = nextFiber.parent;
284
+ }
285
+ return null;
286
+ }
332
287
 
333
- const effectCall = this.effectCalls.find((effect) => effect.id === callbackId);
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 : [];
334
296
 
335
- const executeCallback = () => {
336
- const now = Date.now();
337
- const timeSinceLastCall = now - effectCall.lastCall;
338
-
339
- // Track call counts and handle potential over-calling issues
340
- if (timeSinceLastCall < this.errorThreshold) {
341
- effectCall.count += 1;
342
- if (effectCall.count > this.maxIntervalCalls) {
343
- throw new Error(
344
- `Woah, way too many calls! Ensure you are not over-looping. Adjust maxIntervalCalls and errorThreshold as needed.`
345
- );
346
- }
347
- } else {
348
- effectCall.count = 1;
349
- }
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));
350
301
 
351
- effectCall.lastCall = now;
302
+ reconcileChildren(fiber, children);
303
+ }
352
304
 
353
- setTimeout(() => {
354
- try {
355
- effects.push(callbackId); // Track executed effects
356
- callback(); // Execute the callback
357
- } catch (error) {
358
- console.error(error);
359
- }
360
- }, 0);
361
- };
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
+ }
362
315
 
363
- // First time: Run the effect and mark it as run
364
- if (!effectCall.hasRun && dependencies.length === 0) {
365
- executeCallback();
366
- effectCall.hasRun = true;
367
- effectCall.dependencies = dependencies;
368
- return;
369
- }
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
+ }
370
334
 
371
- // If there are no dependencies, do nothing after the first run
372
- if (dependencies.length === 0) {
373
- return;
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
+ }
374
369
  }
375
370
 
376
- // Check if dependencies have changed
377
- let dependenciesChanged = false;
378
- for (let i = 0; i < dependencies.length; i++) {
379
- const previousDependencies = effectCall.dependencies || [];
380
- if (
381
- JSON.stringify(previousDependencies[i]) !== JSON.stringify(dependencies[i])
382
- ) {
383
- dependenciesChanged = true;
384
- break;
385
- }
371
+ if (prevSibling == null) {
372
+ wipFiber.child = newFiber;
373
+ } else {
374
+ prevSibling.sibling = newFiber;
386
375
  }
376
+ prevSibling = newFiber;
377
+ }
378
+
379
+ oldFibersByKey.forEach(fiber => {
380
+ fiber.effectTag = "DELETION";
381
+ deletions.push(fiber);
382
+ });
387
383
 
388
- // If dependencies have changed, run the effect and update dependencies
389
- if (dependenciesChanged) {
390
- executeCallback();
391
- effectCall.dependencies = dependencies;
384
+ if (prevSibling) prevSibling.sibling = null;
385
+ }
386
+
387
+ /**
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.
393
+ */
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
+ };
412
+ }
413
+
414
+ /**
415
+ * Creates a text virtual DOM element.
416
+ * @param {string} text - The text content.
417
+ * @returns {VNode} The created text element.
418
+ */
419
+ function createTextElement(text: string): VNode {
420
+ return {
421
+ type: "TEXT_ELEMENT",
422
+ props: {
423
+ nodeValue: text,
424
+ children: [],
425
+ },
426
+ };
427
+ }
428
+
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
+ }
441
+
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;
449
+ }
450
+
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);
392
461
  }
462
+ };
463
+
464
+ hookIndex++;
465
+ return [hook.state, setState];
393
466
  }
394
467
 
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
+ }
395
477
 
396
- useState(key, defaultValue, persist = false) {
397
- let value = this.state[key] || defaultValue;
398
- if(value === "true" || value === "false") {
399
- value = JSON.parse(value);
400
- }
401
- // if value is boolean store as string
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]));
402
487
 
403
- if (persist) {
404
- value = sessionStorage.getItem(key) ? JSON.parse(sessionStorage.getItem(key)).value : defaultValue;
488
+ if (hasChanged) {
489
+ if (hook._cleanupFn) {
490
+ hook._cleanupFn();
405
491
  }
406
- const setValue = (newValue) => {
407
- if(typeof newValue === "function") {
408
- newValue = newValue(this.state[key]);
492
+ setTimeout(() => {
493
+ const newCleanup = callback();
494
+ if (typeof newCleanup === 'function') {
495
+ hook._cleanupFn = newCleanup;
496
+ } else {
497
+ hook._cleanupFn = undefined;
409
498
  }
410
- this.state[key] = typeof newValue === "boolean" ? newValue.toString() : newValue;
411
- if (persist) {
412
- sessionStorage.setItem(key, JSON.stringify({ value: newValue }));
413
- }
414
- this.forceUpdate(this.key);
415
- };
416
- return [value, setValue];
417
- }
418
- useFetch(url, options) {
419
- const loadingKey = "loading_" + url;
420
- const errorKey = "error" + url;
421
- const dataKey = "_data" + url;
422
- let [loading, setLoading, _clear1] = this.useState(loadingKey, true);
423
- let [error, setError, _clear2] = this.useState(errorKey, null);
424
- let [data, setData, clear] = this.useState(dataKey, null);
425
- if (loading() && !error() && !data()) {
426
- fetch(url, options).then((res) => res.json()).then((data2) => {
427
- setLoading(false);
428
- setData(data2);
429
- this.forceUpdate(this.key);
430
- setTimeout(() => {
431
- _clear1()
432
- _clear2()
433
- clear()
434
- }, 1500)
435
- }).catch((err) => {
436
- setError(err);
437
- this.forceUpdate(this.key);
438
- });
439
- }
440
- return { loading, error, data };
499
+ }, 0);
500
+ }
501
+
502
+ hook.deps = deps;
503
+ hookIndex++;
504
+ }
505
+
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
+ }
520
+
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
+ }
531
+
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
+ }
542
+
543
+ let hook = wipFiber.hooks[hookIndex];
544
+ if (!hook) {
545
+ hook = { current: initial };
546
+ wipFiber.hooks[hookIndex] = hook;
547
+ }
548
+
549
+ hookIndex++;
550
+ return hook;
551
+ }
552
+
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.");
441
561
  }
442
- addEventListener(element, event, handler) {
443
- if (!this.eventRegistry.has(element)) {
444
- this.eventRegistry.set(element, []);
562
+
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();
445
576
  }
446
- const registeredEvents = this.eventRegistry.get(element);
447
- const isDuplicate = registeredEvents.some((e) => e.type === event && e.handler === handler);
448
- if (!isDuplicate) {
449
- element["on" + event] = handler;
450
- registeredEvents.push({ type: event, handler });
451
- this.eventRegistry.set(element, registeredEvents);
577
+ const cleanup = callback();
578
+ if (typeof cleanup === 'function') {
579
+ hook._cleanupFn = cleanup;
580
+ } else {
581
+ hook._cleanupFn = undefined;
452
582
  }
453
583
  }
454
- removeEventListeners(element) {
455
- // Unregister and remove all events for the element
456
- const registeredEvents = this.eventRegistry.get(element) || [];
457
- registeredEvents.forEach(({ type, handler }) => {
458
- element.removeEventListener(type, handler);
459
- });
460
- this.eventRegistry.delete(element);
461
- }
462
- forceUpdate(key) {
463
- let el = document.querySelector(`[idKey="${key}"]`);
464
- let newl = this.toElement(this.props);
465
- if (newl.getAttribute("idKey") !== key) {
466
- newl = Array.from(newl.children).filter((el2) => el2.getAttribute("idKey") === key)[0];
467
- }
468
- this.Reconciler.update(el, newl);
469
- }
470
- attachEventsRecursively = (element, source) => {
471
- // Rebind events for the current element
472
- const events = this.eventRegistry.get(source) || [];
473
- events.forEach(({ event, handler }) => {
474
- this.addEventListener(element, event, handler);
475
- });
476
-
477
- // Traverse children recursively
478
- const children = Array.from(source.childNodes || []);
479
- const elementChildren = Array.from(element.childNodes || []);
480
-
481
- children.forEach((child, index) => {
482
- if (elementChildren[index]) {
483
- this.attachEventsRecursively(elementChildren[index], child);
484
- }
485
- });
486
- };
487
584
 
585
+ hook.deps = deps;
586
+ hookIndex++;
587
+ }
488
588
 
489
- Reconciler = {
490
- update: (oldElement, newElement) => {
491
- if (!oldElement || !newElement) return;
492
-
493
- // Check if the current element needs an update
494
- if (this.Reconciler.shouldUpdate(oldElement, newElement)) {
495
- // Update attributes
496
- const oldChildren = Array.from(oldElement.childNodes);
497
- const newChildren = Array.from(newElement.childNodes);
498
-
499
- const maxLength = Math.max(oldChildren.length, newChildren.length);
500
- if (oldElement.tagName !== newElement.tagName) {
501
- const newElementClone = newElement.cloneNode(true);
502
- oldElement.replaceWith(newElementClone);
503
-
504
- // Attach events recursively to the new element
505
- this.attachEventsRecursively(newElementClone, newElement);
506
- return;
507
- }
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
+ }
508
604
 
509
- for (let i = 0; i < maxLength; i++) {
510
- if (i >= oldChildren.length) {
511
- const newChildClone = newChildren[i].cloneNode(true);
512
- if(oldElement.nodeType === Node.TEXT_NODE) {
513
- oldElement.textContent = newElement.textContent;
514
- return;
515
- }
516
- oldElement.appendChild(newChildClone);
517
-
518
- // Rebind events to the new child (and its children recursively)
519
- this.attachEventsRecursively(newChildClone, newChildren[i]);
520
- } else if (i >= newChildren.length) {
521
- oldElement.removeChild(oldChildren[i]);
522
- } else {
523
- this.Reconciler.update(oldChildren[i], newChildren[i]);
524
- }
525
- }
526
-
527
- Array.from(oldElement.attributes || []).forEach(({ name }) => {
528
- if (!newElement.hasAttribute(name)) {
529
- oldElement.removeAttribute(name);
530
- }
531
- });
532
-
533
- Array.from(newElement.attributes || []).forEach(({ name, value }) => {
534
- if (oldElement.getAttribute(name) !== value) {
535
- oldElement.setAttribute(name, value);
536
- }
537
- });
538
-
539
- // Handle text node updates
540
- if (oldElement.nodeType === Node.TEXT_NODE) {
541
- if (oldElement.textContent !== newElement.textContent) {
542
- oldElement.textContent = newElement.textContent;
543
- }
544
- return;
545
- }
546
-
547
- // If the element has a single text node, update text directly
548
- if (
549
- oldElement.childNodes.length === 1 &&
550
- oldElement.firstChild.nodeType === Node.TEXT_NODE
551
- ) {
552
- if (oldElement.textContent !== newElement.textContent) {
553
- oldElement.textContent = newElement.textContent;
554
- }
555
- return;
556
- }
557
- }
558
-
559
- // Process children recursively
560
- const oldChildren = Array.from(oldElement.childNodes);
561
- const newChildren = Array.from(newElement.childNodes);
562
-
563
- const maxLength = Math.max(oldChildren.length, newChildren.length);
564
-
565
- for (let i = 0; i < maxLength; i++) {
566
- if (i >= oldChildren.length) {
567
- // Add new child if it exists in newChildren but not in oldChildren
568
- const newChildClone = newChildren[i].cloneNode(true);
569
- oldElement.appendChild(newChildClone);
570
-
571
- // Attach any event listeners
572
- const newChildEvents = this.eventRegistry.get(newChildren[i]) || [];
573
- newChildEvents.forEach(({ type, handler }) => {
574
- this.addEventListener(newChildClone, type, handler);
575
- });
576
- } else if (i >= newChildren.length) {
577
- // Remove child if it exists in oldChildren but not in newChildren
578
- oldElement.removeChild(oldChildren[i]);
579
- } else {
580
- this.Reconciler.update(oldChildren[i], newChildren[i]);
581
- }
582
- }
583
-
584
- // Reapply events for the current element
585
- const parentEvents = this.eventRegistry.get(newElement) || [];
586
- parentEvents.forEach(({ type, handler }) => {
587
- if (newElement.nodeType === oldElement.nodeType) {
588
- this.addEventListener(oldElement, type, handler);
589
- }
590
- });
605
+ let hook = wipFiber.hooks[hookIndex];
606
+ if (!hook) {
607
+ hook = {
608
+ state: initialState,
609
+ queue: [],
610
+ };
611
+ wipFiber.hooks[hookIndex] = hook;
612
+ }
591
613
 
592
- },
593
- shouldUpdate: (oldElement, newElement) => {
594
- // Check if node types differ
595
- if (oldElement.nodeType !== newElement.nodeType) {
596
- return true;
597
- }
598
-
599
- // Check if text content differs
600
- if (oldElement.nodeType === Node.TEXT_NODE) {
601
- return oldElement.textContent !== newElement.textContent;
602
- }
603
-
604
- // Check if node names differ
605
- if (oldElement.nodeName !== newElement.nodeName) {
606
- return true;
607
- }
608
-
609
- // Check if child counts differ
610
- if (oldElement.childNodes.length !== newElement.childNodes.length) {
611
- return true;
612
- }
613
-
614
- // Check if attributes differ
615
- const newAttributes = Array.from(newElement.attributes || []);
616
- for (let { name, value } of newAttributes) {
617
- if (oldElement.getAttribute(name) !== value) {
618
- return true;
619
- }
620
- }
621
-
622
- // If no differences found, no update needed
623
- return false;
624
- },
625
- }
626
-
627
- parseToElement = (element) => {
628
- if (!element || element.nodeType) return ""
629
-
630
- let svgTags = ["svg", "path", "circle", "rect", "line", "polyline", "polygon", "ellipse", "g"];
631
- let isSvg = svgTags.includes(element.type);
632
-
633
- // Create the element, using proper namespace for SVG
634
- let el = isSvg
635
- ? document.createElementNS("http://www.w3.org/2000/svg", element.type)
636
- : document.createElement(element.type);
637
-
638
- // Handle text nodes
639
- if (typeof element === "string" || typeof element === "number" || typeof element === "boolean") {
640
- el.textContent = element; // Safer alternative to innerHTML
641
- return el;
642
- }
643
-
644
- // Set attributes
645
- let attributes = element.props || {};
646
- for (let key in attributes) {
647
- if(key === "ref") {
648
- let _key = attributes[key].key;
649
- // update the ref
650
- let ref = this.refs.find((r) => r.key == _key);
651
- if(ref) {
652
- ref.current = document.querySelector(`[idKey="${_key}"]`) || el;
653
- }
654
- el.setAttribute("idKey", _key);
655
- element.props.idKey = _key
656
- }
657
- else if (key === "key") {
658
- el.key = attributes[key];
659
- } else if (key === "className") {
660
- el.setAttribute("class", attributes[key]);
661
- } else if (key === "style") {
662
- let styleObject = attributes[key];
663
- if (typeof styleObject === "object") {
664
- for (let styleKey in styleObject) {
665
- el.style[styleKey] = styleObject[styleKey];
666
- }
667
- }
668
- } else if (key.startsWith("on")) {
669
- // Event listeners
670
- const eventType = key.substring(2).toLowerCase();
671
- const handler = attributes[key];
672
- this.eventRegistry.set(el, [...(this.eventRegistry.get(el) || []), { event: eventType, handler }]);
673
- this.addEventListener(el, eventType, handler);
674
- } else if (attributes[key] !== null && attributes[key] !== undefined &&
675
- !key.includes(" ") || !key.includes("-") || !key.includes("_")) {
676
- try {
677
- el.setAttribute(key, attributes[key]);
678
- } catch (error) {
679
-
680
- }
681
- } else if(typeof attributes[key] === "object" && key !== "style"){
682
- continue;
683
- }
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);
684
624
  }
685
-
686
- // Handle children
687
- let children = element.children || [];
688
- children.forEach((child) => {
689
- if (Array.isArray(child)) {
690
- // Recursively process nested arrays
691
- child.forEach((nestedChild) => el.appendChild(this.parseToElement(nestedChild)));
692
- } else if (typeof child === "function") {
693
- // Handle functional components
694
- let component = memoizeClassComponent(Component, child.name);
695
- component.Mounted = true;
696
- component.render = (props) => child(props);
697
- let componentElement = component.toElement();
698
- el.appendChild(componentElement);
699
- } else if (typeof child === "object") {
700
- // Nested object children
701
- el.appendChild(this.parseToElement(child));
702
- } else if (child !== null && child !== undefined && child !== false) {
703
- // Text nodes
704
- el.appendChild(document.createTextNode(child));
705
- }
706
- });
707
-
708
- return el;
709
625
  };
710
- e(element, props, ...children) {
711
- if (typeof element === "function") {
712
- return element();
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;
713
646
  }
714
- return { type: element, props: props || {}, children: children || [] };
647
+ fiber = fiber.parent;
715
648
  }
716
- toElement() {
717
- let children = this.render(this.props);
718
- let el = this.parseToElement(children);
719
- el.setAttribute("idKey", this.key);
720
- return el;
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
+ },
670
+ };
671
+ context.Provider._context = context;
672
+ return context;
673
+ }
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
+ }
686
+
687
+ let hook = wipFiber.hooks[hookIndex];
688
+ if (!hook) {
689
+ hook = { memoizedValue: factory(), deps };
690
+ wipFiber.hooks[hookIndex] = hook;
721
691
  }
722
- render() {
723
- return "";
692
+
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;
724
699
  }
700
+
701
+ hookIndex++;
702
+ return hook.memoizedValue;
725
703
  }
726
704
 
727
- function memoizeClassComponent(Component, key) {
728
- let instance = memoizes.get(key);
729
- if (!instance) {
730
- instance = new Component(key);
731
- memoizes.set(key, instance);
732
- }
733
- return instance;
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 };
734
748
  }
749
+
735
750
  /**
736
- * @description - Render jsx Componenet to the DOM
737
- * @param element
738
- * @param container
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.
739
754
  */
740
- export function render(element, container) {
741
- // CLEAR STATE ON RELOAD
742
- if (!isServer) {
743
- window.addEventListener("beforeunload", () => {
744
- let keys = Object.keys(sessionStorage);
745
- keys.forEach((key) => {
746
- if (key.startsWith("state_")) {
747
- sessionStorage.removeItem(key);
748
- }
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);
819
+ return;
820
+ }
821
+ }
822
+
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}`);
828
+ }
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
749
838
  });
750
- });
751
- }
752
- if (isClassComponent(element)) {
753
- const instance = new element;
754
- instance.Mounted = true;
755
- let el = instance.toElement();
756
- instance.element = el;
757
- container.innerHTML = "";
758
- container.replaceWith(el);
759
- } else {
760
- let memoizedInstance = memoizeClassComponent(Component, element.name);
761
- memoizedInstance.Mounted = true;
762
- element = element.bind(memoizedInstance);
839
+ }
840
+
841
+ setData(result);
842
+ } catch (err) {
843
+ setError(err instanceof Error ? err : new Error(String(err)));
844
+ } finally {
845
+ setLoading(false);
846
+ }
847
+ }, [url, mergedCacheOptions]); // This dependency is now stable
848
+
849
+ useEffect(() => {
850
+ fetchData();
851
+ }, [fetchData]); // This dependency is now stable
852
+
853
+ return { data, loading, error, refetch: fetchData };
854
+ }
855
+
856
+ /**
857
+ * A hook for tracking window focus state.
858
+ * @returns {boolean} True if the window is focused.
859
+ */
860
+ export function useWindowFocus(): boolean {
861
+ const [isFocused, setIsFocused] = useState(true);
862
+
863
+ useEffect(() => {
864
+ const onFocus = () => setIsFocused(true);
865
+ const onBlur = () => setIsFocused(false);
763
866
 
764
- memoizedInstance.render = (props) => element(props);
765
- if (element.name == "default") {
766
- 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;
767
893
  }
768
- memoizedInstance.key = element.name;
769
- let el = memoizedInstance.toElement();
770
- el.key = element.name;
771
- container.innerHTML = "";
772
- container.replaceWith(el);
773
- }
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]);
774
925
  }