@ipxjs/refract 0.7.0 → 0.9.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ipxjs/refract",
3
- "version": "0.7.0",
3
+ "version": "0.9.0",
4
4
  "description": "A minimal React-like virtual DOM library with an optional React compat layer",
5
5
  "type": "module",
6
6
  "main": "src/refract/index.ts",
@@ -52,6 +52,7 @@ export function renderFiber(vnode: VNode, container: Node): void {
52
52
  isRendering = false;
53
53
  const committedDeletions = deletions.slice();
54
54
  commitRoot(rootFiber);
55
+ clearAlternates(rootFiber);
55
56
  runAfterCommitHandlers();
56
57
  roots.set(container, rootFiber);
57
58
  runCommitHandlers(rootFiber, committedDeletions);
@@ -309,6 +310,17 @@ function setRef(ref: unknown, value: Node | null): void {
309
310
  }
310
311
  }
311
312
 
313
+ /** Release alternate references after commit to prevent memory leaks.
314
+ * Alternates are only needed during reconciliation; retaining them
315
+ * creates an ever-growing chain of old fiber trees. */
316
+ function clearAlternates(fiber: Fiber | null): void {
317
+ while (fiber) {
318
+ fiber.alternate = null;
319
+ if (fiber.child) clearAlternates(fiber.child);
320
+ fiber = fiber.sibling;
321
+ }
322
+ }
323
+
312
324
  function commitDeletion(fiber: Fiber): void {
313
325
  runCleanups(fiber);
314
326
  // Clear ref on unmount
@@ -406,6 +418,7 @@ function flushRenders(): void {
406
418
  isRendering = false;
407
419
  const committedDeletions = deletions.slice();
408
420
  commitRoot(newRoot);
421
+ clearAlternates(newRoot);
409
422
  runAfterCommitHandlers();
410
423
  roots.set(container, newRoot);
411
424
  runCommitHandlers(newRoot, committedDeletions);
@@ -75,8 +75,10 @@ export function useEffect(effect: () => EffectCleanup, deps?: unknown[]): void {
75
75
  hook.state.deps = deps;
76
76
  hook.state.pending = true;
77
77
  markPendingEffects(fiber);
78
- } else {
79
- hook.state.pending = false;
78
+ } else if (hook.state.pending) {
79
+ // Re-render before effect was flushed — keep it pending and
80
+ // re-register the fiber so the deferred flush still finds it.
81
+ markPendingEffects(fiber);
80
82
  }
81
83
  }
82
84
  }
@@ -94,8 +96,8 @@ export function useLayoutEffect(effect: () => EffectCleanup, deps?: unknown[]):
94
96
  hook.state.deps = deps;
95
97
  hook.state.pending = true;
96
98
  markPendingLayoutEffects(fiber);
97
- } else {
98
- hook.state.pending = false;
99
+ } else if (hook.state.pending) {
100
+ markPendingLayoutEffects(fiber);
99
101
  }
100
102
  }
101
103
  }
@@ -113,8 +115,8 @@ export function useInsertionEffect(effect: () => EffectCleanup, deps?: unknown[]
113
115
  hook.state.deps = deps;
114
116
  hook.state.pending = true;
115
117
  markPendingInsertionEffects(fiber);
116
- } else {
117
- hook.state.pending = false;
118
+ } else if (hook.state.pending) {
119
+ markPendingInsertionEffects(fiber);
118
120
  }
119
121
  }
120
122
  }
@@ -33,14 +33,16 @@ function cleanupFiberEffects(fiber: Fiber): void {
33
33
  if (!fiber.hooks) return;
34
34
 
35
35
  for (const hook of fiber.hooks) {
36
- const state = hook.state as { cleanup?: () => void; pending?: boolean } | undefined;
37
- if (state?.cleanup) {
38
- state.cleanup();
39
- state.cleanup = undefined;
36
+ const state = hook.state;
37
+ if (!state || typeof state !== "object") continue;
38
+ const effectState = state as { cleanup?: () => void; pending?: boolean };
39
+ if (effectState.cleanup) {
40
+ effectState.cleanup();
41
+ effectState.cleanup = undefined;
40
42
  }
41
43
  // Prevent deferred passive effects from running on unmounted fibers
42
- if (state && "pending" in state) {
43
- state.pending = false;
44
+ if ("pending" in effectState) {
45
+ effectState.pending = false;
44
46
  }
45
47
  }
46
48
  }