vaderjs 2.3.11 → 2.3.12

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.
Files changed (3) hide show
  1. package/index.ts +342 -152
  2. package/main.js +305 -152
  3. package/package.json +1 -1
package/index.ts CHANGED
@@ -30,7 +30,8 @@ interface Fiber {
30
30
  effectTag?: "PLACEMENT" | "UPDATE" | "DELETION";
31
31
  hooks?: Hook[];
32
32
  key?: string | number | null;
33
- propsCache?: Record<string, any>;
33
+ ref?: any;
34
+ propsCache?: Record<string, any>;
34
35
  __compareProps?: (prev: any, next: any) => boolean;
35
36
  __skipMemo?: boolean;
36
37
  _needsUpdate?: boolean;
@@ -43,6 +44,7 @@ export interface VNode {
43
44
  [key: string]: any;
44
45
  };
45
46
  key?: string | number | null;
47
+ ref?: any; // ✅ Add this for ref support
46
48
  }
47
49
 
48
50
  interface Hook {
@@ -61,12 +63,55 @@ interface Hook {
61
63
  */
62
64
  const isEvent = (key: string) => key.startsWith("on");
63
65
 
66
+ function shouldSetAsProperty(name: string, isSvg: boolean): boolean {
67
+ // These should always be set as properties (when possible)
68
+ const propertyNames = [
69
+ 'value', 'checked', 'selected', 'disabled', 'readOnly',
70
+ 'multiple', 'muted', 'defaultChecked', 'defaultValue'
71
+ ];
72
+
73
+ // These should always be set as attributes
74
+ const attributeNames = [
75
+ 'aria-', 'data-', 'role', 'tabindex', 'for', 'class', 'style',
76
+ 'id', 'name', 'type', 'placeholder', 'href', 'src', 'alt',
77
+ 'title', 'width', 'height', 'viewBox', 'fill', 'stroke'
78
+ ];
79
+
80
+ // Check if it's a boolean attribute
81
+ if (name in dom && typeof (dom as any)[name] === 'boolean') {
82
+ return true;
83
+ }
84
+
85
+ // Check property list
86
+ if (propertyNames.includes(name)) {
87
+ return true;
88
+ }
89
+
90
+ // Check attribute patterns
91
+ if (attributeNames.some(attr => name.startsWith(attr)) || name.includes('-')) {
92
+ return false;
93
+ }
94
+
95
+ // For SVG, prefer attributes
96
+ if (isSvg) {
97
+ return false;
98
+ }
99
+
100
+ // Default to property if it exists on the DOM element
101
+ return name in dom;
102
+ }
64
103
  /**
65
104
  * Checks if a property key is a regular property (not children or event).
66
105
  * @param {string} key - The property key to check.
67
106
  * @returns {boolean} True if the key is a regular property.
68
107
  */
69
- const isProperty = (key: string) => key !== "children" && !isEvent(key);
108
+ const isProperty = (key: string) =>
109
+ key !== "children" &&
110
+ !isEvent(key) &&
111
+ key !== "ref" &&
112
+ key !== "key" &&
113
+ key !== "__source" &&
114
+ key !== "__self";
70
115
 
71
116
  /**
72
117
  * Creates a function to check if a property has changed between objects.
@@ -91,11 +136,11 @@ const isGone = (prev: object, next: object) => (key: string) => !(key in next);
91
136
  */
92
137
  function createDom(fiber: Fiber): Node {
93
138
  let dom: Node;
139
+ const isSvg = isSvgElement(fiber);
94
140
 
95
141
  if (fiber.type === "TEXT_ELEMENT") {
96
- dom = document.createTextNode("");
142
+ dom = document.createTextNode(fiber.props.nodeValue || "");
97
143
  } else {
98
- const isSvg = isSvgElement(fiber);
99
144
  if (isSvg) {
100
145
  dom = document.createElementNS("http://www.w3.org/2000/svg", fiber.type as string);
101
146
  } else {
@@ -103,13 +148,10 @@ function createDom(fiber: Fiber): Node {
103
148
  }
104
149
  }
105
150
 
106
- updateDom(dom, {}, fiber.props);
107
-
108
- // Assign ref if this fiber has a ref prop
109
- if (fiber.props && fiber.props.ref) {
110
- fiber.props.ref.current = dom;
111
- }
112
-
151
+ // Update props (attributes, events, etc.)
152
+ updateDom(dom, {}, fiber.props, isSvg);
153
+
154
+ fiber.dom = dom;
113
155
  return dom;
114
156
  }
115
157
 
@@ -126,34 +168,44 @@ function isSvgElement(fiber: Fiber): boolean {
126
168
  }
127
169
 
128
170
 
129
- /**
171
+ /**
130
172
  * Applies updated props to a DOM node.
131
173
  * @param {Node} dom - The DOM node to update.
132
174
  * @param {object} prevProps - The previous properties.
133
175
  * @param {object} nextProps - The new properties.
134
176
  */
135
177
  function updateDom(dom: Node, prevProps: any, nextProps: any): void {
136
- prevProps = prevProps || {};
178
+ prevProps = prevProps || {};
137
179
  nextProps = nextProps || {};
138
180
 
139
181
  const isSvg = dom instanceof SVGElement;
182
+
183
+ if (dom.nodeType === Node.TEXT_NODE) {
184
+ if (prevProps.nodeValue !== nextProps.nodeValue) {
185
+ (dom as Text).nodeValue = nextProps.nodeValue;
186
+ }
187
+ return;
188
+ }
140
189
 
141
- // Handle ref updates
190
+ // Handle ref updates - IMPORTANT: This must come BEFORE event listeners
142
191
  if (prevProps.ref && prevProps.ref !== nextProps.ref) {
143
- prevProps.ref.current = null;
192
+ if (prevProps.ref.current === dom) {
193
+ prevProps.ref.current = null;
194
+ }
144
195
  }
145
196
  if (nextProps.ref && nextProps.ref !== prevProps.ref) {
146
197
  nextProps.ref.current = dom;
147
- }
198
+ }
148
199
 
149
- // Remove old or changed event listeners
200
+ // Remove old event listeners
150
201
  Object.keys(prevProps)
151
- .filter(isEvent)
202
+ .filter(key => key.startsWith("on"))
152
203
  .filter(key => !(key in nextProps) || isNew(prevProps, nextProps)(key))
153
204
  .forEach(name => {
154
205
  const eventType = name.toLowerCase().substring(2);
155
- if (typeof prevProps[name] === 'function') {
156
- (dom as Element).removeEventListener(eventType, prevProps[name]);
206
+ const handler = prevProps[name];
207
+ if (typeof handler === 'function') {
208
+ (dom as Element).removeEventListener(eventType, handler);
157
209
  }
158
210
  });
159
211
 
@@ -166,12 +218,10 @@ function updateDom(dom: Node, prevProps: any, nextProps: any): void {
166
218
  (dom as Element).removeAttribute('class');
167
219
  } else if (name === 'style') {
168
220
  (dom as HTMLElement).style.cssText = '';
221
+ } else if (name in dom && !isSvg) {
222
+ (dom as any)[name] = '';
169
223
  } else {
170
- if (isSvg) {
171
- (dom as Element).removeAttribute(name);
172
- } else {
173
- (dom as any)[name] = '';
174
- }
224
+ (dom as Element).removeAttribute(name);
175
225
  }
176
226
  });
177
227
 
@@ -180,51 +230,52 @@ function updateDom(dom: Node, prevProps: any, nextProps: any): void {
180
230
  .filter(isProperty)
181
231
  .filter(isNew(prevProps, nextProps))
182
232
  .forEach(name => {
233
+ const value = nextProps[name];
234
+
183
235
  if (name === 'style') {
184
- const style = nextProps[name];
185
- if (typeof style === 'string') {
186
- (dom as HTMLElement).style.cssText = style;
187
- } else if (typeof style === 'object' && style !== null) {
188
- for (const [key, value] of Object.entries(style)) {
189
- (dom as HTMLElement).style[key] = value;
236
+ if (typeof value === 'string') {
237
+ (dom as HTMLElement).style.cssText = value;
238
+ } else if (typeof value === 'object' && value !== null) {
239
+ for (const [key, val] of Object.entries(value)) {
240
+ const cssKey = key.replace(/[A-Z]/g, match => `-${match.toLowerCase()}`);
241
+ (dom as HTMLElement).style[cssKey as any] = val;
190
242
  }
191
243
  }
192
244
  } else if (name === 'className' || name === 'class') {
193
- (dom as Element).setAttribute('class', nextProps[name]);
194
- } else {
195
- if (isSvg) {
196
- (dom as Element).setAttribute(name, nextProps[name]);
245
+ (dom as Element).setAttribute('class', value);
246
+ } else if (typeof value === 'boolean') {
247
+ if (value) {
248
+ (dom as Element).setAttribute(name, '');
197
249
  } else {
198
- (dom as any)[name] = nextProps[name];
250
+ (dom as Element).removeAttribute(name);
251
+ }
252
+ } else if (name.includes('-') || isSvg) {
253
+ (dom as Element).setAttribute(name, value);
254
+ } else if (name in dom && !isSvg) {
255
+ try {
256
+ (dom as any)[name] = value;
257
+ } catch {
258
+ (dom as Element).setAttribute(name, value);
199
259
  }
260
+ } else {
261
+ (dom as Element).setAttribute(name, value);
200
262
  }
201
263
  });
202
264
 
203
- // Add new event listeners
265
+ // Add new event listeners - This is the crucial part!
204
266
  Object.keys(nextProps)
205
- .filter(isEvent)
267
+ .filter(key => key.startsWith("on"))
206
268
  .filter(isNew(prevProps, nextProps))
207
269
  .forEach(name => {
208
270
  const eventType = name.toLowerCase().substring(2);
209
271
  const handler = nextProps[name];
210
272
  if (typeof handler === 'function') {
211
- (dom as Element).addEventListener(eventType, handler);
212
- }
213
- });
214
-
215
- Object.keys(nextProps)
216
- .filter(isEvent)
217
- .filter(isNew(prevProps, nextProps))
218
- .forEach(name => {
219
- const eventType = name.toLowerCase().substring(2);
220
- const handler = nextProps[name];
221
- if (typeof handler === 'function') {
222
- // Remove old listener first if it exists
273
+ // Remove any existing listener first
223
274
  if (prevProps[name]) {
224
- dom.removeEventListener(eventType, prevProps[name]);
275
+ (dom as Element).removeEventListener(eventType, prevProps[name]);
225
276
  }
226
- // Add new listener with passive: true for better performance
227
- dom.addEventListener(eventType, handler, { passive: true });
277
+ // Add the new listener
278
+ (dom as Element).addEventListener(eventType, handler);
228
279
  }
229
280
  });
230
281
  }
@@ -245,7 +296,7 @@ function commitRoot(): void {
245
296
  * Recursively commits a fiber and its children to the DOM.
246
297
  * @param {Fiber} fiber - The fiber to commit.
247
298
  */
248
- function commitWork(fiber: Fiber | null): void {
299
+ function commitWork(fiber: Fiber | null): void {
249
300
  if (!fiber) return;
250
301
 
251
302
  let domParentFiber = fiber.parent;
@@ -255,46 +306,69 @@ function commitWork(fiber: Fiber | null): void {
255
306
  const domParent = domParentFiber?.dom ?? null;
256
307
 
257
308
  if (fiber.effectTag === "PLACEMENT" && fiber.dom != null) {
258
- if (domParent) domParent.appendChild(fiber.dom);
259
-
260
- // ⚡ Assign refs immediately when DOM is placed
261
- if (fiber.props && fiber.props.ref) {
262
- fiber.props.ref.current = fiber.dom;
309
+ if (domParent) {
310
+ domParent.appendChild(fiber.dom);
311
+ }
312
+ // Assign ref from fiber
313
+ if (fiber.ref && fiber.dom) {
314
+ assignRef(fiber.ref, fiber.dom);
263
315
  }
316
+ } else if (fiber.effectTag === "UPDATE" && fiber.dom != null) {
317
+ const prevProps = fiber.alternate?.props ?? {};
318
+ const nextProps = fiber.props;
319
+ updateDom(fiber.dom, prevProps, nextProps);
264
320
 
265
- // Also check for useRef hooks in this fiber
266
- if (fiber.hooks) {
267
- for (const hook of fiber.hooks) {
268
- if ("current" in hook && !hook._isRef && fiber.dom) {
269
- // This is likely a DOM ref hook
270
- hook.current = fiber.dom;
271
- }
321
+ // Handle ref updates from fiber
322
+ const prevRef = fiber.alternate?.ref;
323
+ const nextRef = fiber.ref;
324
+
325
+ if (prevRef !== nextRef) {
326
+ if (prevRef) {
327
+ assignRef(prevRef, null);
328
+ }
329
+ if (nextRef && fiber.dom) {
330
+ assignRef(nextRef, fiber.dom);
272
331
  }
332
+ } else if (nextRef && fiber.dom) {
333
+ // Ensure ref is still set
334
+ assignRef(nextRef, fiber.dom);
273
335
  }
274
- } else if (fiber.effectTag === "UPDATE" && fiber.dom != null) {
275
- updateDom(fiber.dom, fiber.alternate?.props ?? {}, fiber.props);
276
336
  } else if (fiber.effectTag === "DELETION") {
277
337
  commitDeletion(fiber);
338
+ return;
278
339
  }
279
340
 
280
341
  commitWork(fiber.child);
281
342
  commitWork(fiber.sibling);
282
343
  }
283
344
 
345
+ // ✅ Helper function to assign refs safely
346
+ function assignRef(ref: any, dom: Node | null): void {
347
+ if (typeof ref === 'function') {
348
+ ref(dom);
349
+ } else if (ref && typeof ref === 'object') {
350
+ ref.current = dom;
351
+ }
352
+ }
353
+
284
354
 
285
355
  /**
286
356
  * Recursively removes a fiber and its children from the DOM.
287
357
  * @param {Fiber} fiber - The fiber to remove.
288
358
  */
289
- function commitDeletion(fiber: Fiber | null): void {
290
- if (!fiber) {
291
- return;
292
- }
359
+ function commitDeletion(fiber: Fiber | null): void {
360
+ if (!fiber) return;
293
361
 
294
- // Clear refs when element is removed
295
- if (fiber.props && fiber.props.ref) {
296
- fiber.props.ref.current = null;
297
- }
362
+ // Clear refs recursively
363
+ const clearRefs = (f: Fiber) => {
364
+ if (f.ref) {
365
+ assignRef(f.ref, null);
366
+ }
367
+ if (f.child) clearRefs(f.child);
368
+ if (f.sibling) clearRefs(f.sibling);
369
+ };
370
+
371
+ clearRefs(fiber);
298
372
 
299
373
  if (fiber.dom) {
300
374
  if (fiber.dom.parentNode) {
@@ -329,7 +403,8 @@ export function render(element: VNode, container: Node): void {
329
403
  * The main work loop for rendering and reconciliation.
330
404
  */
331
405
  function workLoop(): void {
332
- if (!wipRoot && currentRoot) {
406
+ // If there's a scheduled render but no wipRoot, create one
407
+ if (!nextUnitOfWork && !wipRoot && currentRoot) {
333
408
  wipRoot = {
334
409
  dom: currentRoot.dom,
335
410
  props: currentRoot.props,
@@ -338,11 +413,13 @@ function workLoop(): void {
338
413
  deletions = [];
339
414
  nextUnitOfWork = wipRoot;
340
415
  }
341
-
416
+
417
+ // Perform work
342
418
  while (nextUnitOfWork) {
343
419
  nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
344
420
  }
345
421
 
422
+ // Commit changes if we've finished all work
346
423
  if (!nextUnitOfWork && wipRoot) {
347
424
  commitRoot();
348
425
  }
@@ -382,22 +459,45 @@ function performUnitOfWork(fiber: Fiber): Fiber | null {
382
459
  * @param {Fiber} fiber - The function component fiber to update.
383
460
  */
384
461
  function updateFunctionComponent(fiber: Fiber) {
462
+ // Store the current fiber globally for hooks
385
463
  wipFiber = fiber;
386
464
  hookIndex = 0;
387
- fiber.hooks = fiber.alternate?.hooks || [];
388
-
389
- const rawChildren = (fiber.type as Function)(fiber.props);
390
- const children = normalizeChildren(rawChildren, fiber);
465
+
466
+ // Initialize hooks array if needed
467
+ if (!fiber.hooks) {
468
+ fiber.hooks = [];
469
+ }
470
+
471
+ // Copy hooks from alternate if it exists
472
+ if (fiber.alternate?.hooks) {
473
+ fiber.hooks = fiber.alternate.hooks.map(hook => ({ ...hook }));
474
+ }
391
475
 
392
- reconcileChildren(fiber, children);
476
+ // Call the component function
477
+ const children = (fiber.type as Function)(fiber.props);
478
+
479
+ // Normalize and reconcile children
480
+ const normalizedChildren = normalizeChildren(children, fiber);
481
+ reconcileChildren(fiber, normalizedChildren);
482
+
483
+ // Clean up
484
+ wipFiber = null;
393
485
  }
394
486
  function normalizeChildren(children: any, parentFiber: Fiber): VNode[] {
395
487
  if (!children) return [];
488
+
489
+ // Handle arrays, single elements, and conditional rendering
396
490
  let arr = Array.isArray(children) ? children : [children];
397
-
491
+
398
492
  return arr.flatMap((child, index) => {
399
- if (child == null || typeof child === "boolean") return [];
400
- if (typeof child === "string" || typeof child === "number") return [createTextElement(String(child))];
493
+ // Skip null, undefined, false, true (conditional rendering)
494
+ if (child == null || typeof child === "boolean") {
495
+ return [];
496
+ }
497
+
498
+ if (typeof child === "string" || typeof child === "number") {
499
+ return [createTextElement(String(child))];
500
+ }
401
501
 
402
502
  // Ensure every child has a stable key
403
503
  const key = child.key ?? child.props?.id ?? `${parentFiber.key ?? "root"}-${index}`;
@@ -425,63 +525,82 @@ function reconcileChildren(wipFiber: Fiber, elements: VNode[]) {
425
525
  let oldFiber = wipFiber.alternate?.child;
426
526
  let prevSibling: Fiber | null = null;
427
527
 
528
+ // Build a map of existing fibers by key
428
529
  const existingFibers = new Map<string | number | null, Fiber>();
429
530
  while (oldFiber) {
430
- const key = oldFiber.key ?? index;
531
+ const key = oldFiber.key ?? `index-${index}`;
431
532
  existingFibers.set(key, oldFiber);
432
533
  oldFiber = oldFiber.sibling;
433
534
  index++;
434
535
  }
435
536
 
436
537
  index = 0;
437
- for (; index < elements.length; index++) {
438
- const element = elements[index];
439
- const key = element.key ?? index;
538
+ let newChildFiber: Fiber | null = null;
539
+
540
+ // Process each element
541
+ for (let i = 0; i < elements.length; i++) {
542
+ const element = elements[i];
543
+
544
+ // Skip null/false/undefined (conditional rendering)
545
+ if (element == null) {
546
+ continue;
547
+ }
548
+
549
+ const key = element.key ?? `index-${index}`;
440
550
  const oldFiber = existingFibers.get(key);
441
-
442
- const sameType = oldFiber && element && element.type === oldFiber.type;
443
- let newFiber: Fiber | null = null;
444
-
551
+
552
+ const sameType = oldFiber && element.type === oldFiber.type;
553
+
445
554
  if (sameType) {
446
- // Reuse old fiber for same type + key
447
- newFiber = {
448
- ...oldFiber,
555
+ // Update existing fiber
556
+ newChildFiber = {
557
+ type: oldFiber.type,
449
558
  props: element.props,
559
+ dom: oldFiber.dom,
450
560
  parent: wipFiber,
451
561
  alternate: oldFiber,
452
562
  effectTag: "UPDATE",
453
- _needsUpdate: false,
563
+ key,
564
+ ref: element.ref,
565
+ hooks: oldFiber.hooks,
454
566
  };
567
+
455
568
  existingFibers.delete(key);
456
- } else if (element) {
569
+ } else {
457
570
  // Create new fiber
458
- newFiber = {
459
- type: element.type,
460
- props: element.props,
461
- dom: null,
462
- parent: wipFiber,
463
- alternate: null,
464
- effectTag: "PLACEMENT",
465
- key,
466
- _needsUpdate: true,
467
- };
468
- }
469
-
470
- if (oldFiber && !sameType) {
471
- oldFiber.effectTag = "DELETION";
472
- deletions.push(oldFiber);
571
+ if (element) {
572
+ newChildFiber = {
573
+ type: element.type,
574
+ props: element.props,
575
+ dom: null,
576
+ parent: wipFiber,
577
+ alternate: null,
578
+ effectTag: "PLACEMENT",
579
+ key,
580
+ ref: element.ref,
581
+ };
582
+ }
583
+
584
+ // Mark old fiber for deletion if it exists
585
+ if (oldFiber) {
586
+ oldFiber.effectTag = "DELETION";
587
+ deletions.push(oldFiber);
588
+ }
473
589
  }
474
-
475
- if (index === 0) {
476
- wipFiber.child = newFiber;
477
- } else if (prevSibling && newFiber) {
478
- prevSibling.sibling = newFiber;
590
+
591
+ // Link the new fiber into the tree
592
+ if (newChildFiber) {
593
+ if (index === 0) {
594
+ wipFiber.child = newChildFiber;
595
+ } else if (prevSibling) {
596
+ prevSibling.sibling = newChildFiber;
597
+ }
598
+ prevSibling = newChildFiber;
599
+ index++;
479
600
  }
480
-
481
- if (newFiber) prevSibling = newFiber;
482
601
  }
483
-
484
- // Mark remaining old fibers for deletion
602
+
603
+ // Mark any remaining old fibers for deletion
485
604
  existingFibers.forEach(fiber => {
486
605
  fiber.effectTag = "DELETION";
487
606
  deletions.push(fiber);
@@ -495,20 +614,33 @@ function reconcileChildren(wipFiber: Fiber, elements: VNode[]) {
495
614
  * @param {...any} children - The element's children.
496
615
  * @returns {VNode} The created virtual DOM element.
497
616
  */
498
- export function createElement(type: string | Function, props?: any, ...children: any[]): VNode {
617
+ export function createElement(type: string | Function, props?: any, ...children: any[]): VNode {
499
618
  const rawChildren = children.flat().filter(c => c != null && typeof c !== "boolean");
500
619
  const normalizedChildren = rawChildren.map((child, i) => {
501
620
  if (typeof child === "object") return child;
502
621
  return createTextElement(String(child));
503
622
  });
504
623
 
624
+ // Extract ref from props (if it exists)
625
+ const ref = props?.ref;
626
+
627
+ // Create a new props object without the ref
628
+ const elementProps = { ...props };
629
+ if ('ref' in elementProps) {
630
+ delete elementProps.ref;
631
+ }
632
+
633
+ // Add children back
634
+ elementProps.children = normalizedChildren;
635
+
505
636
  return {
506
637
  type,
507
- props: { ...props, children: normalizedChildren },
638
+ props: elementProps,
508
639
  key: props?.key ?? props?.id ?? null,
640
+ // Store the ref separately on the VNode
641
+ ref,
509
642
  };
510
643
  }
511
-
512
644
  /**
513
645
  * Creates a text virtual DOM element.
514
646
  * @param {string} text - The text content.
@@ -524,6 +656,21 @@ function createTextElement(text: string): VNode {
524
656
  };
525
657
  }
526
658
 
659
+ export function useStableRef<T>(initialValue: T | null = null): { current: T | null } {
660
+ const ref = useRef(initialValue);
661
+
662
+ // Use effect to ensure ref is cleaned up on unmount
663
+ useEffect(() => {
664
+ return () => {
665
+ // Cleanup ref when component unmounts
666
+ if (ref.current !== null) {
667
+ ref.current = null;
668
+ }
669
+ };
670
+ }, []);
671
+
672
+ return ref;
673
+ }
527
674
  /**
528
675
  * A React-like useState hook for managing component state.
529
676
  * @template T
@@ -533,41 +680,60 @@ function createTextElement(text: string): VNode {
533
680
 
534
681
 
535
682
 
536
- export function useState<T>(initial: T | (() => T)): [T, (action: T | ((prevState: T) => T)) => void] {
683
+ // Add this to your useState hook to ensure re-renders
684
+ export function useState<T>(initial: T | (() => T)): [T, (action: T | ((prevState: T) => T)) => void] {
537
685
  if (!wipFiber) {
538
686
  throw new Error("Hooks can only be called inside a Vader.js function component.");
539
687
  }
540
688
 
541
- let hook = wipFiber.hooks[hookIndex];
689
+ const currentHookIndex = hookIndex;
690
+ let hook = wipFiber.hooks[currentHookIndex];
691
+
542
692
  if (!hook) {
543
693
  hook = {
544
694
  state: typeof initial === "function" ? (initial as () => T)() : initial,
545
695
  queue: [],
546
696
  _needsUpdate: false
547
697
  };
548
- wipFiber.hooks[hookIndex] = hook;
698
+ wipFiber.hooks[currentHookIndex] = hook;
549
699
  }
550
700
 
551
701
  const setState = (action: T | ((prevState: T) => T)) => {
552
- // Calculate new state based on current state
702
+ // Calculate new state
553
703
  const newState = typeof action === "function"
554
704
  ? (action as (prevState: T) => T)(hook.state)
555
705
  : action;
556
706
 
557
- hook.state = newState;
558
-
559
- // Reset work-in-progress root to trigger re-r
560
-
561
- deletions = [];
562
- nextUnitOfWork = wipRoot;
707
+ if (!Object.is(hook.state, newState)) {
708
+ hook.state = newState;
563
709
 
564
- // Start the render process
565
- requestAnimationFrame(workLoop);
710
+ // Schedule a re-render starting from the component's fiber
711
+ scheduleRender();
712
+ }
566
713
  };
567
714
 
568
715
  hookIndex++;
569
716
  return [hook.state, setState];
570
717
  }
718
+
719
+ /**
720
+ * Schedules a re-render of the entire app
721
+ */
722
+ function scheduleRender(): void {
723
+ if (!currentRoot || !currentRoot.dom) return;
724
+
725
+ // Schedule a new render starting from the current root
726
+ wipRoot = {
727
+ dom: currentRoot.dom,
728
+ props: currentRoot.props,
729
+ alternate: currentRoot,
730
+ };
731
+ deletions = [];
732
+ nextUnitOfWork = wipRoot;
733
+
734
+ // Start the work loop on next animation frame
735
+ requestAnimationFrame(workLoop);
736
+ }
571
737
 
572
738
  /**
573
739
  * A React-like useEffect hook for side effects.
@@ -637,13 +803,23 @@ export function Match({ when, children }: { when: boolean, children: VNode[] }):
637
803
  * @param {Function} fn - The function component to wrap.
638
804
  * @returns {Function} A function that creates a VNode when called with props.
639
805
  */
640
- export function component<P extends object>(
641
- fn: (props: P) => VNode | VNode[]
642
- ): (props: P & { key?: string | number }) => VNode {
643
- return (props: P & { key?: string | number }) => {
644
- return createElement(fn, props);
806
+ export function component<P extends object>(
807
+ fn: (props: P & { children?: any }) => VNode | VNode[]
808
+ ): (props: P & { key?: string | number; children?: any }) => VNode {
809
+ return (props: P & { key?: string | number; children?: any }) => {
810
+ // Merge key if needed
811
+ const { key, ...rest } = props;
812
+ // Call the component function directly
813
+ const vnode = fn(rest as P & { children?: any });
814
+
815
+ // Attach the key to the VNode
816
+ if (vnode && typeof vnode === "object") {
817
+ vnode.key = key;
818
+ }
819
+ return vnode;
645
820
  };
646
821
  }
822
+
647
823
  export function Show({ when, children }: { when: boolean, children: VNode[] }): VNode | null {
648
824
  return when ? children : null;
649
825
  }
@@ -669,21 +845,22 @@ declare global {
669
845
  * @param {T} initial - The initial reference value.
670
846
  * @returns {{current: T}} A mutable ref object.
671
847
  */
672
- export function useRef<T>(initial: T): { current: T } {
848
+ export function useRef<T>(initial: T | null = null): { current: T | null } {
673
849
  if (!wipFiber) {
674
850
  throw new Error("Hooks can only be called inside a Vader.js function component.");
675
851
  }
676
852
 
677
853
  let hook = wipFiber.hooks[hookIndex];
678
854
  if (!hook) {
679
- hook = { current: initial, _isRef: true };
855
+ hook = { current: initial };
680
856
  wipFiber.hooks[hookIndex] = hook;
681
857
  }
682
858
 
683
859
  hookIndex++;
684
- return hook;
860
+ return hook as { current: T | null };
685
861
  }
686
862
 
863
+
687
864
  /**
688
865
  * A React-like useLayoutEffect hook that runs synchronously after DOM mutations.
689
866
  * @param {Function} callback - The effect callback.
@@ -1047,14 +1224,27 @@ export function useLocalStorage<T>(key: string, initialValue: T): [T, (value: T)
1047
1224
  export function useOnClickOutside(ref: { current: HTMLElement | null }, handler: Function): void {
1048
1225
  useEffect(() => {
1049
1226
  const listener = (event: MouseEvent) => {
1050
- if (ref.current && !ref.current.contains(event.target as Node)) {
1227
+ // Create a stable reference to the current ref value
1228
+ const currentRef = ref.current;
1229
+ console.log(currentRef)
1230
+
1231
+ if (!currentRef || !event.target) {
1232
+ return;
1233
+ }
1234
+
1235
+ console.log(currentRef)
1236
+
1237
+ // Check if click is outside
1238
+ if (!currentRef.contains(event.target as Node)) {
1051
1239
  handler(event);
1052
1240
  }
1053
1241
  };
1054
- document.addEventListener("mousedown", listener);
1242
+
1243
+ // Use capture phase to ensure we catch the event
1244
+ document.addEventListener("mousedown", listener, true);
1245
+
1055
1246
  return () => {
1056
- document.removeEventListener("mousedown", listener);
1247
+ document.removeEventListener("mousedown", listener, true);
1057
1248
  };
1058
- }, [ref, handler]);
1059
- }
1060
-
1249
+ }, [ref, handler]); // Keep ref in dependencies
1250
+ }