vaderjs 2.3.11 → 2.3.13

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 +437 -164
  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);
199
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);
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)
206
- .filter(isNew(prevProps, nextProps))
207
- .forEach(name => {
208
- const eventType = name.toLowerCase().substring(2);
209
- const handler = nextProps[name];
210
- if (typeof handler === 'function') {
211
- (dom as Element).addEventListener(eventType, handler);
212
- }
213
- });
214
-
215
- Object.keys(nextProps)
216
- .filter(isEvent)
267
+ .filter(key => key.startsWith("on"))
217
268
  .filter(isNew(prevProps, nextProps))
218
269
  .forEach(name => {
219
270
  const eventType = name.toLowerCase().substring(2);
220
271
  const handler = nextProps[name];
221
272
  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
  }
@@ -236,16 +287,57 @@ function updateDom(dom: Node, prevProps: any, nextProps: any): void {
236
287
  function commitRoot(): void {
237
288
  deletions.forEach(commitWork);
238
289
  commitWork(wipRoot.child);
290
+
291
+ // Run effects after DOM is committed
292
+ runEffects(wipRoot);
293
+
239
294
  currentRoot = wipRoot;
240
295
  wipRoot = null;
241
296
  isRenderScheduled = false;
242
297
  }
243
298
 
299
+ function runEffects(fiber: Fiber | null): void {
300
+ if (!fiber) return;
301
+
302
+ // Run effects for this fiber
303
+ if (fiber._pendingEffects) {
304
+ fiber._pendingEffects.forEach(({ callback, cleanup, hookIndex }) => {
305
+ // Run cleanup from previous effect
306
+ if (cleanup) {
307
+ try {
308
+ cleanup();
309
+ } catch (err) {
310
+ console.error('Error in effect cleanup:', err);
311
+ }
312
+ }
313
+
314
+ // Run the new effect
315
+ try {
316
+ const newCleanup = callback();
317
+
318
+ // Store the cleanup function in the hook
319
+ if (fiber.hooks && fiber.hooks[hookIndex]) {
320
+ fiber.hooks[hookIndex]._cleanupFn = typeof newCleanup === 'function' ? newCleanup : undefined;
321
+ }
322
+ } catch (err) {
323
+ console.error('Error in effect:', err);
324
+ }
325
+ });
326
+
327
+ // Clear pending effects
328
+ fiber._pendingEffects = [];
329
+ }
330
+
331
+ // Recursively run effects for children
332
+ runEffects(fiber.child);
333
+ runEffects(fiber.sibling);
334
+ }
335
+
244
336
  /**
245
337
  * Recursively commits a fiber and its children to the DOM.
246
338
  * @param {Fiber} fiber - The fiber to commit.
247
339
  */
248
- function commitWork(fiber: Fiber | null): void {
340
+ function commitWork(fiber: Fiber | null): void {
249
341
  if (!fiber) return;
250
342
 
251
343
  let domParentFiber = fiber.parent;
@@ -255,46 +347,69 @@ function commitWork(fiber: Fiber | null): void {
255
347
  const domParent = domParentFiber?.dom ?? null;
256
348
 
257
349
  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;
350
+ if (domParent) {
351
+ domParent.appendChild(fiber.dom);
352
+ }
353
+ // Assign ref from fiber
354
+ if (fiber.ref && fiber.dom) {
355
+ assignRef(fiber.ref, fiber.dom);
263
356
  }
357
+ } else if (fiber.effectTag === "UPDATE" && fiber.dom != null) {
358
+ const prevProps = fiber.alternate?.props ?? {};
359
+ const nextProps = fiber.props;
360
+ updateDom(fiber.dom, prevProps, nextProps);
264
361
 
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
- }
362
+ // Handle ref updates from fiber
363
+ const prevRef = fiber.alternate?.ref;
364
+ const nextRef = fiber.ref;
365
+
366
+ if (prevRef !== nextRef) {
367
+ if (prevRef) {
368
+ assignRef(prevRef, null);
272
369
  }
370
+ if (nextRef && fiber.dom) {
371
+ assignRef(nextRef, fiber.dom);
372
+ }
373
+ } else if (nextRef && fiber.dom) {
374
+ // Ensure ref is still set
375
+ assignRef(nextRef, fiber.dom);
273
376
  }
274
- } else if (fiber.effectTag === "UPDATE" && fiber.dom != null) {
275
- updateDom(fiber.dom, fiber.alternate?.props ?? {}, fiber.props);
276
377
  } else if (fiber.effectTag === "DELETION") {
277
378
  commitDeletion(fiber);
379
+ return;
278
380
  }
279
381
 
280
382
  commitWork(fiber.child);
281
383
  commitWork(fiber.sibling);
282
384
  }
283
385
 
386
+ // ✅ Helper function to assign refs safely
387
+ function assignRef(ref: any, dom: Node | null): void {
388
+ if (typeof ref === 'function') {
389
+ ref(dom);
390
+ } else if (ref && typeof ref === 'object') {
391
+ ref.current = dom;
392
+ }
393
+ }
394
+
284
395
 
285
396
  /**
286
397
  * Recursively removes a fiber and its children from the DOM.
287
398
  * @param {Fiber} fiber - The fiber to remove.
288
399
  */
289
- function commitDeletion(fiber: Fiber | null): void {
290
- if (!fiber) {
291
- return;
292
- }
400
+ function commitDeletion(fiber: Fiber | null): void {
401
+ if (!fiber) return;
293
402
 
294
- // Clear refs when element is removed
295
- if (fiber.props && fiber.props.ref) {
296
- fiber.props.ref.current = null;
297
- }
403
+ // Clear refs recursively
404
+ const clearRefs = (f: Fiber) => {
405
+ if (f.ref) {
406
+ assignRef(f.ref, null);
407
+ }
408
+ if (f.child) clearRefs(f.child);
409
+ if (f.sibling) clearRefs(f.sibling);
410
+ };
411
+
412
+ clearRefs(fiber);
298
413
 
299
414
  if (fiber.dom) {
300
415
  if (fiber.dom.parentNode) {
@@ -329,7 +444,8 @@ export function render(element: VNode, container: Node): void {
329
444
  * The main work loop for rendering and reconciliation.
330
445
  */
331
446
  function workLoop(): void {
332
- if (!wipRoot && currentRoot) {
447
+ // If there's a scheduled render but no wipRoot, create one
448
+ if (!nextUnitOfWork && !wipRoot && currentRoot) {
333
449
  wipRoot = {
334
450
  dom: currentRoot.dom,
335
451
  props: currentRoot.props,
@@ -338,11 +454,13 @@ function workLoop(): void {
338
454
  deletions = [];
339
455
  nextUnitOfWork = wipRoot;
340
456
  }
341
-
457
+
458
+ // Perform work
342
459
  while (nextUnitOfWork) {
343
460
  nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
344
461
  }
345
462
 
463
+ // Commit changes if we've finished all work
346
464
  if (!nextUnitOfWork && wipRoot) {
347
465
  commitRoot();
348
466
  }
@@ -382,22 +500,50 @@ function performUnitOfWork(fiber: Fiber): Fiber | null {
382
500
  * @param {Fiber} fiber - The function component fiber to update.
383
501
  */
384
502
  function updateFunctionComponent(fiber: Fiber) {
503
+ // Store the current fiber globally for hooks
385
504
  wipFiber = fiber;
386
505
  hookIndex = 0;
387
- fiber.hooks = fiber.alternate?.hooks || [];
388
-
389
- const rawChildren = (fiber.type as Function)(fiber.props);
390
- const children = normalizeChildren(rawChildren, fiber);
506
+
507
+ // Initialize hooks array if needed
508
+ if (!fiber.hooks) {
509
+ fiber.hooks = [];
510
+ }
511
+
512
+ // Copy hooks from alternate but ensure they're fresh
513
+ if (fiber.alternate?.hooks) {
514
+ // Create new hooks array with same structure
515
+ fiber.hooks = fiber.alternate.hooks.map(altHook => {
516
+ // Create a shallow copy to avoid mutation issues
517
+ const newHook = { ...altHook };
518
+ return newHook;
519
+ });
520
+ }
391
521
 
392
- reconcileChildren(fiber, children);
522
+ // Call the component function
523
+ const children = (fiber.type as Function)(fiber.props);
524
+
525
+ // Normalize and reconcile children
526
+ const normalizedChildren = normalizeChildren(children, fiber);
527
+ reconcileChildren(fiber, normalizedChildren);
528
+
529
+ // Clean up
530
+ wipFiber = null;
393
531
  }
394
532
  function normalizeChildren(children: any, parentFiber: Fiber): VNode[] {
395
533
  if (!children) return [];
534
+
535
+ // Handle arrays, single elements, and conditional rendering
396
536
  let arr = Array.isArray(children) ? children : [children];
397
-
537
+
398
538
  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))];
539
+ // Skip null, undefined, false, true (conditional rendering)
540
+ if (child == null || typeof child === "boolean") {
541
+ return [];
542
+ }
543
+
544
+ if (typeof child === "string" || typeof child === "number") {
545
+ return [createTextElement(String(child))];
546
+ }
401
547
 
402
548
  // Ensure every child has a stable key
403
549
  const key = child.key ?? child.props?.id ?? `${parentFiber.key ?? "root"}-${index}`;
@@ -425,63 +571,82 @@ function reconcileChildren(wipFiber: Fiber, elements: VNode[]) {
425
571
  let oldFiber = wipFiber.alternate?.child;
426
572
  let prevSibling: Fiber | null = null;
427
573
 
574
+ // Build a map of existing fibers by key
428
575
  const existingFibers = new Map<string | number | null, Fiber>();
429
576
  while (oldFiber) {
430
- const key = oldFiber.key ?? index;
577
+ const key = oldFiber.key ?? `index-${index}`;
431
578
  existingFibers.set(key, oldFiber);
432
579
  oldFiber = oldFiber.sibling;
433
580
  index++;
434
581
  }
435
582
 
436
583
  index = 0;
437
- for (; index < elements.length; index++) {
438
- const element = elements[index];
439
- const key = element.key ?? index;
584
+ let newChildFiber: Fiber | null = null;
585
+
586
+ // Process each element
587
+ for (let i = 0; i < elements.length; i++) {
588
+ const element = elements[i];
589
+
590
+ // Skip null/false/undefined (conditional rendering)
591
+ if (element == null) {
592
+ continue;
593
+ }
594
+
595
+ const key = element.key ?? `index-${index}`;
440
596
  const oldFiber = existingFibers.get(key);
441
-
442
- const sameType = oldFiber && element && element.type === oldFiber.type;
443
- let newFiber: Fiber | null = null;
444
-
597
+
598
+ const sameType = oldFiber && element.type === oldFiber.type;
599
+
445
600
  if (sameType) {
446
- // Reuse old fiber for same type + key
447
- newFiber = {
448
- ...oldFiber,
601
+ // Update existing fiber
602
+ newChildFiber = {
603
+ type: oldFiber.type,
449
604
  props: element.props,
605
+ dom: oldFiber.dom,
450
606
  parent: wipFiber,
451
607
  alternate: oldFiber,
452
608
  effectTag: "UPDATE",
453
- _needsUpdate: false,
609
+ key,
610
+ ref: element.ref,
611
+ hooks: oldFiber.hooks,
454
612
  };
613
+
455
614
  existingFibers.delete(key);
456
- } else if (element) {
615
+ } else {
457
616
  // 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);
617
+ if (element) {
618
+ newChildFiber = {
619
+ type: element.type,
620
+ props: element.props,
621
+ dom: null,
622
+ parent: wipFiber,
623
+ alternate: null,
624
+ effectTag: "PLACEMENT",
625
+ key,
626
+ ref: element.ref,
627
+ };
628
+ }
629
+
630
+ // Mark old fiber for deletion if it exists
631
+ if (oldFiber) {
632
+ oldFiber.effectTag = "DELETION";
633
+ deletions.push(oldFiber);
634
+ }
473
635
  }
474
-
475
- if (index === 0) {
476
- wipFiber.child = newFiber;
477
- } else if (prevSibling && newFiber) {
478
- prevSibling.sibling = newFiber;
636
+
637
+ // Link the new fiber into the tree
638
+ if (newChildFiber) {
639
+ if (index === 0) {
640
+ wipFiber.child = newChildFiber;
641
+ } else if (prevSibling) {
642
+ prevSibling.sibling = newChildFiber;
643
+ }
644
+ prevSibling = newChildFiber;
645
+ index++;
479
646
  }
480
-
481
- if (newFiber) prevSibling = newFiber;
482
647
  }
483
-
484
- // Mark remaining old fibers for deletion
648
+
649
+ // Mark any remaining old fibers for deletion
485
650
  existingFibers.forEach(fiber => {
486
651
  fiber.effectTag = "DELETION";
487
652
  deletions.push(fiber);
@@ -495,20 +660,33 @@ function reconcileChildren(wipFiber: Fiber, elements: VNode[]) {
495
660
  * @param {...any} children - The element's children.
496
661
  * @returns {VNode} The created virtual DOM element.
497
662
  */
498
- export function createElement(type: string | Function, props?: any, ...children: any[]): VNode {
663
+ export function createElement(type: string | Function, props?: any, ...children: any[]): VNode {
499
664
  const rawChildren = children.flat().filter(c => c != null && typeof c !== "boolean");
500
665
  const normalizedChildren = rawChildren.map((child, i) => {
501
666
  if (typeof child === "object") return child;
502
667
  return createTextElement(String(child));
503
668
  });
504
669
 
670
+ // Extract ref from props (if it exists)
671
+ const ref = props?.ref;
672
+
673
+ // Create a new props object without the ref
674
+ const elementProps = { ...props };
675
+ if ('ref' in elementProps) {
676
+ delete elementProps.ref;
677
+ }
678
+
679
+ // Add children back
680
+ elementProps.children = normalizedChildren;
681
+
505
682
  return {
506
683
  type,
507
- props: { ...props, children: normalizedChildren },
684
+ props: elementProps,
508
685
  key: props?.key ?? props?.id ?? null,
686
+ // Store the ref separately on the VNode
687
+ ref,
509
688
  };
510
689
  }
511
-
512
690
  /**
513
691
  * Creates a text virtual DOM element.
514
692
  * @param {string} text - The text content.
@@ -524,50 +702,122 @@ function createTextElement(text: string): VNode {
524
702
  };
525
703
  }
526
704
 
705
+ export function useStableRef<T>(initialValue: T | null = null): { current: T | null } {
706
+ const ref = useRef(initialValue);
707
+
708
+ // Use effect to ensure ref is cleaned up on unmount
709
+ useEffect(() => {
710
+ return () => {
711
+ // Cleanup ref when component unmounts
712
+ if (ref.current !== null) {
713
+ ref.current = null;
714
+ }
715
+ };
716
+ }, []);
717
+
718
+ return ref;
719
+ }
527
720
  /**
528
721
  * A React-like useState hook for managing component state.
529
722
  * @template T
530
723
  * @param {T|(() => T)} initial - The initial state value or initializer function.
531
724
  * @returns {[T, (action: T | ((prevState: T) => T)) => void]} A stateful value and a function to update it.
532
725
  */
533
-
534
726
 
535
-
536
- export function useState<T>(initial: T | (() => T)): [T, (action: T | ((prevState: T) => T)) => void] {
727
+ export function useState<T>(initial: T | (() => T)): [T, (action: T | ((prevState: T) => T)) => void] {
537
728
  if (!wipFiber) {
538
729
  throw new Error("Hooks can only be called inside a Vader.js function component.");
539
730
  }
540
731
 
541
- let hook = wipFiber.hooks[hookIndex];
732
+ const currentHookIndex = hookIndex;
733
+ const currentFiber = wipFiber; // Capture current fiber
734
+
735
+ let hook = currentFiber.hooks[currentHookIndex];
736
+
542
737
  if (!hook) {
543
738
  hook = {
544
739
  state: typeof initial === "function" ? (initial as () => T)() : initial,
545
740
  queue: [],
546
741
  _needsUpdate: false
547
742
  };
548
- wipFiber.hooks[hookIndex] = hook;
743
+ currentFiber.hooks[currentHookIndex] = hook;
549
744
  }
550
745
 
746
+ // Create setState that captures current hook and fiber
551
747
  const setState = (action: T | ((prevState: T) => T)) => {
552
- // Calculate new state based on current state
553
- const newState = typeof action === "function"
554
- ? (action as (prevState: T) => T)(hook.state)
555
- : action;
556
-
557
- hook.state = newState;
748
+ // Use setTimeout to ensure we're outside the current render cycle
749
+ setTimeout(() => {
750
+ // Re-find the hook in the current fiber (in case it moved)
751
+ const fiber = currentRoot || wipRoot;
752
+ if (!fiber) return;
558
753
 
559
- // Reset work-in-progress root to trigger re-r
754
+ // Find the component fiber that owns this hook
755
+ let targetFiber = findFiberWithHook(fiber, currentFiber, currentHookIndex);
756
+ if (!targetFiber || !targetFiber.hooks) return;
560
757
 
561
- deletions = [];
562
- nextUnitOfWork = wipRoot;
758
+ const targetHook = targetFiber.hooks[currentHookIndex];
759
+ if (!targetHook) return;
563
760
 
564
- // Start the render process
565
- requestAnimationFrame(workLoop);
761
+ // Calculate new state
762
+ const newState = typeof action === "function"
763
+ ? (action as (prevState: T) => T)(targetHook.state)
764
+ : action;
765
+
766
+ if (!Object.is(targetHook.state, newState)) {
767
+ targetHook.state = newState;
768
+ targetHook._needsUpdate = true;
769
+
770
+ // Schedule a re-render
771
+ scheduleRender();
772
+ }
773
+ }, 0);
566
774
  };
567
775
 
568
776
  hookIndex++;
569
777
  return [hook.state, setState];
570
778
  }
779
+
780
+ // Helper to find the fiber containing a specific hook
781
+ function findFiberWithHook(root: Fiber, targetFiber: Fiber, hookIndex: number): Fiber | null {
782
+ // Simple BFS to find the fiber
783
+ let queue: Fiber[] = [root];
784
+
785
+ while (queue.length > 0) {
786
+ const fiber = queue.shift()!;
787
+
788
+ // Check if this is our target fiber
789
+ if (fiber === targetFiber ||
790
+ (fiber.type === targetFiber.type &&
791
+ fiber.key === targetFiber.key)) {
792
+ return fiber;
793
+ }
794
+
795
+ // Add children to queue
796
+ if (fiber.child) queue.push(fiber.child);
797
+ if (fiber.sibling) queue.push(fiber.sibling);
798
+ }
799
+
800
+ return null;
801
+ }
802
+
803
+ /**
804
+ * Schedules a re-render of the entire app
805
+ */
806
+ function scheduleRender(): void {
807
+ if (!currentRoot || !currentRoot.dom) return;
808
+
809
+ // Schedule a new render starting from the current root
810
+ wipRoot = {
811
+ dom: currentRoot.dom,
812
+ props: currentRoot.props,
813
+ alternate: currentRoot,
814
+ };
815
+ deletions = [];
816
+ nextUnitOfWork = wipRoot;
817
+
818
+ // Start the work loop on next animation frame
819
+ requestAnimationFrame(workLoop);
820
+ }
571
821
 
572
822
  /**
573
823
  * A React-like useEffect hook for side effects.
@@ -590,15 +840,14 @@ export function useEffect(callback: Function, deps?: any[]): void {
590
840
  deps.some((dep, i) => !Object.is(dep, hook.deps[i]));
591
841
 
592
842
  if (hasChanged) {
593
- if (hook._cleanupFn) {
594
- hook._cleanupFn();
595
- }
843
+ // Schedule effect to run after render
596
844
  setTimeout(() => {
597
- const newCleanup = callback();
598
- if (typeof newCleanup === 'function') {
599
- hook._cleanupFn = newCleanup;
600
- } else {
601
- hook._cleanupFn = undefined;
845
+ if (hook._cleanupFn) {
846
+ hook._cleanupFn();
847
+ }
848
+ const cleanup = callback();
849
+ if (typeof cleanup === 'function') {
850
+ hook._cleanupFn = cleanup;
602
851
  }
603
852
  }, 0);
604
853
  }
@@ -637,13 +886,23 @@ export function Match({ when, children }: { when: boolean, children: VNode[] }):
637
886
  * @param {Function} fn - The function component to wrap.
638
887
  * @returns {Function} A function that creates a VNode when called with props.
639
888
  */
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);
889
+ export function component<P extends object>(
890
+ fn: (props: P & { children?: any }) => VNode | VNode[]
891
+ ): (props: P & { key?: string | number; children?: any }) => VNode {
892
+ return (props: P & { key?: string | number; children?: any }) => {
893
+ // Merge key if needed
894
+ const { key, ...rest } = props;
895
+ // Call the component function directly
896
+ const vnode = fn(rest as P & { children?: any });
897
+
898
+ // Attach the key to the VNode
899
+ if (vnode && typeof vnode === "object") {
900
+ vnode.key = key;
901
+ }
902
+ return vnode;
645
903
  };
646
904
  }
905
+
647
906
  export function Show({ when, children }: { when: boolean, children: VNode[] }): VNode | null {
648
907
  return when ? children : null;
649
908
  }
@@ -669,21 +928,22 @@ declare global {
669
928
  * @param {T} initial - The initial reference value.
670
929
  * @returns {{current: T}} A mutable ref object.
671
930
  */
672
- export function useRef<T>(initial: T): { current: T } {
931
+ export function useRef<T>(initial: T | null = null): { current: T | null } {
673
932
  if (!wipFiber) {
674
933
  throw new Error("Hooks can only be called inside a Vader.js function component.");
675
934
  }
676
935
 
677
936
  let hook = wipFiber.hooks[hookIndex];
678
937
  if (!hook) {
679
- hook = { current: initial, _isRef: true };
938
+ hook = { current: initial };
680
939
  wipFiber.hooks[hookIndex] = hook;
681
940
  }
682
941
 
683
942
  hookIndex++;
684
- return hook;
943
+ return hook as { current: T | null };
685
944
  }
686
945
 
946
+
687
947
  /**
688
948
  * A React-like useLayoutEffect hook that runs synchronously after DOM mutations.
689
949
  * @param {Function} callback - The effect callback.
@@ -1047,14 +1307,27 @@ export function useLocalStorage<T>(key: string, initialValue: T): [T, (value: T)
1047
1307
  export function useOnClickOutside(ref: { current: HTMLElement | null }, handler: Function): void {
1048
1308
  useEffect(() => {
1049
1309
  const listener = (event: MouseEvent) => {
1050
- if (ref.current && !ref.current.contains(event.target as Node)) {
1310
+ // Create a stable reference to the current ref value
1311
+ const currentRef = ref.current;
1312
+ console.log(currentRef)
1313
+
1314
+ if (!currentRef || !event.target) {
1315
+ return;
1316
+ }
1317
+
1318
+ console.log(currentRef)
1319
+
1320
+ // Check if click is outside
1321
+ if (!currentRef.contains(event.target as Node)) {
1051
1322
  handler(event);
1052
1323
  }
1053
1324
  };
1054
- document.addEventListener("mousedown", listener);
1325
+
1326
+ // Use capture phase to ensure we catch the event
1327
+ document.addEventListener("mousedown", listener, true);
1328
+
1055
1329
  return () => {
1056
- document.removeEventListener("mousedown", listener);
1330
+ document.removeEventListener("mousedown", listener, true);
1057
1331
  };
1058
- }, [ref, handler]);
1059
- }
1060
-
1332
+ }, [ref, handler]); // Keep ref in dependencies
1333
+ }