Skip to content

Commit 41f00b8

Browse files
updated the logic for createEffect to run after the component mounts when inside FC
1 parent b4aae0a commit 41f00b8

File tree

9 files changed

+173
-77
lines changed

9 files changed

+173
-77
lines changed

examples/css/src/Timer.tsx

Lines changed: 9 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,23 @@
1-
import { createSignal, createEffect, cleanUp } from "refract-js";
1+
import { createSignal, createEffect, cleanUp, createRef } from "refract-js";
22

33
function TimerComponent() {
44
const seconds = createSignal<number>(0);
5-
const other = createSignal([1, 2, 3]);
6-
7-
const interval = setInterval(() => {
8-
seconds.update((seconds) => seconds + 1);
9-
}, 1000);
5+
const ref = createRef();
106

117
createEffect(() => {
12-
console.log("Effect");
13-
other.update((prev) => prev.push(4));
8+
console.log(ref.current);
9+
const interval = setInterval(() => {
10+
seconds.update((seconds) => seconds + 1);
11+
}, 1000);
12+
1413
// Cleanup function to clear the interval when the component unmounts
1514
return () => {
1615
console.log("cleanup");
16+
clearInterval(interval);
1717
};
1818
});
1919

20-
cleanUp(() => {
21-
// console.log("FC cleanup");
22-
clearInterval(interval);
23-
});
24-
25-
return (
26-
<p>
27-
Elapsed Time: {() => seconds.value}
28-
{() => other.value.map((i) => i)} seconds
29-
</p>
30-
);
20+
return <p ref={ref}>Elapsed Time: {() => seconds.value}</p>;
3121
}
3222

3323
export default TimerComponent;

readme.md

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,9 @@ render(<App />, document.getElementById("root"));
4646

4747
### State Management
4848

49-
In Refract-js, functions serve as an intuitive way to create dynamic nodes.
50-
Functional components rerender only once; after that, signals handle the DOM reconciliation process efficiently.
49+
In Refract-js, functions serve as an intuitive way to create dynamic nodes.
50+
51+
> Functional components rerender only once;after that, signals handle the DOM reconciliation process efficiently.
5152
5253
You can use
5354
`createSignal()`
@@ -63,7 +64,11 @@ function Counter() {
6364
<div>
6465
{/* Functions for dynamic values that depend on signals */}
6566
<p>Count: {() => count.value}</p>
66-
<button onClick={() => count.value++}>Increment</button>
67+
68+
{/* use the update function to update the count.value */}
69+
<button onClick={() => count.update((prevCount) => prevCount + 1)}>
70+
Increment
71+
</button>
6772
</div>
6873
);
6974
}

src/components/Timer.tsx

Lines changed: 33 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -4,53 +4,50 @@ import {
44
cleanUp,
55
createPromise,
66
computed,
7+
createRef,
78
} from "../index";
89

910
function TimerComponent() {
10-
const seconds = createSignal<number>(0);
11+
const ref = createRef();
1112
const other = createSignal({ a: [-1], b: 2, c: 3 });
12-
// const promise = createPromise(
13-
// () =>
14-
// new Promise<string>((resolve) =>
15-
// setTimeout(() => resolve("hello"), 1000)
16-
// )
17-
// );
18-
const seconds2 = computed(() => other.value.a[0]);
19-
// const interval = setInterval(() => {
20-
// // other.update((other) => other.a++);
21-
// seconds.update((seconds) => seconds + 1);
22-
// }, 1000);
13+
14+
const seconds = createSignal<number>(0);
15+
const seconds2 = computed(() => {
16+
console.log("hello");
17+
return seconds.value * 2;
18+
});
19+
2320
createEffect(() => {
24-
// seconds.update((prev) => prev + 1);
2521
// seconds.value;
26-
console.log(other.value);
22+
// seconds.update((prev) => prev + 1);
23+
// console.log(ref.current);
24+
console.log(seconds2.value);
25+
// console.log("first");
2726
return () => {
2827
console.log("cleanup");
2928
};
3029
});
31-
32-
// cleanUp(() => {
33-
// // console.log("FC cleanup");
34-
// clearInterval(interval);
35-
// });
36-
30+
// console.log("inside fc");
3731
return (
38-
<p id="p">
39-
Elapsed Time: {() => seconds2.value}
40-
{/* {() => other.value.a.map((i) => <p>{i}</p>)} seconds */}
41-
<button
42-
onClick={() => {
43-
// other.update((other) => other.a.push(seconds.value));
44-
// other.value.a.push(seconds.value);
45-
// seconds.update((prev) => prev + 1);
46-
other.update((prev) => {
47-
prev.a[0]++;
48-
});
49-
}}
50-
>
51-
Click
52-
</button>
53-
</p>
32+
<>
33+
<p id="p">
34+
Elapsed Time: {() => seconds.value}
35+
Computed Time: {() => seconds2.value}
36+
<button
37+
onClick={() => {
38+
// other.update((prev) => {
39+
// prev.a[0]++;
40+
// });
41+
seconds.update((prev) => prev + 1);
42+
}}
43+
>
44+
Click
45+
</button>
46+
</p>
47+
{/* <div>
48+
<p ref={ref}>I am the last child of TimerComponent</p>
49+
</div> */}
50+
</>
5451
);
5552
}
5653

src/rendering/functionalComponents.ts

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
1-
import { BaseSignal } from "../signals/signal";
1+
import { BaseSignal, runEffect } from "../signals/signal";
2+
import { Fiber } from "../types";
23

3-
let currentFC = null;
4+
let currentFC: Fiber | null = null;
45
let fcMap = new WeakMap<
5-
Function,
6+
Fiber,
67
{
78
signals: Set<BaseSignal<any>>;
89
cleanup: Function[];
910
effects: Set<Function>;
1011
}
1112
>();
1213

13-
export function setCurrentFC(fc) {
14+
export function setCurrentFC(fc: Fiber) {
1415
currentFC = fc;
1516
}
1617

@@ -21,6 +22,16 @@ export function getCurrentFC() {
2122
return currentFC;
2223
}
2324

25+
export function runAllEffects(FC: Fiber) {
26+
if (fcMap.has(FC)) {
27+
const fcData = fcMap.get(FC)!;
28+
29+
for (const effect of fcData.effects) {
30+
runEffect(effect, FC);
31+
}
32+
}
33+
}
34+
2435
export function cleanUp(fn: Function) {
2536
if (currentFC) {
2637
// console.log(currentFC, fcMap.has(currentFC));
@@ -37,6 +48,22 @@ export function cleanUp(fn: Function) {
3748
}
3849
}
3950
}
51+
export function cleanUpWFiber(fn: Function, fiber: Fiber) {
52+
if (fiber) {
53+
// console.log(currentFC, fcMap.has(currentFC));
54+
if (fcMap.has(fiber)) {
55+
const fcData = fcMap.get(fiber)!;
56+
57+
fcData.cleanup.push(fn);
58+
} else {
59+
fcMap.set(fiber, {
60+
signals: new Set(),
61+
cleanup: [fn],
62+
effects: new Set(),
63+
});
64+
}
65+
}
66+
}
4067

4168
export function addEffect(fn: Function) {
4269
if (currentFC) {

src/rendering/render.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
import {
1616
cleanUpFC,
1717
clearCurrentFC,
18+
runAllEffects,
1819
setCurrentFC,
1920
} from "./functionalComponents";
2021

@@ -50,10 +51,20 @@ let elements: Fiber[] = [];
5051
let rootContainer: HTMLElement | null = null;
5152
let rootFragment: DocumentFragment | null = null;
5253
let startTime = -1;
54+
let effectQueue: Fiber[] = [];
55+
56+
function processEffectQueue() {
57+
for (let i = 0; i < effectQueue.length; i++) {
58+
const fiber = effectQueue[i];
59+
runAllEffects(fiber);
60+
}
61+
effectQueue.length = 0;
62+
}
5363

5464
function workLoop(deadline: IdleDeadline) {
5565
if (startTime === -1) startTime = performance.now();
5666

67+
processEffectQueue();
5768
let shouldYield = false;
5869
while (elements.length > 0 && !shouldYield) {
5970
const element = elements.pop();
@@ -62,6 +73,7 @@ function workLoop(deadline: IdleDeadline) {
6273
}
6374

6475
if (elements.length == 0) {
76+
processEffectQueue();
6577
commitRootFragment();
6678
return;
6779
}
@@ -94,7 +106,7 @@ function renderNode(fiber: Fiber) {
94106

95107
const children = fiber.type(fiber.props);
96108
clearCurrentFC();
97-
// fiber.type = "FRAGMENT";
109+
98110
if (Array.isArray(children)) {
99111
// which means that the FC returned a fragment
100112
// console.log(children);
@@ -108,6 +120,8 @@ function renderNode(fiber: Fiber) {
108120
fiber.props.children.push(children);
109121
elements.push(children);
110122
}
123+
// queue to run its effects
124+
effectQueue.push(fiber);
111125
} else {
112126
if (!fiber.dom) fiber.dom = createNode(fiber);
113127
let fiberParent: Fiber | undefined = fiber.parent;
@@ -195,6 +209,10 @@ function commitFiber(
195209
fiber.props.children.push(children);
196210
commitFiber(children, referenceNode, replace, true, customParent);
197211
}
212+
// queue to run its effects at the end of current stack
213+
queueMicrotask(() => {
214+
runAllEffects(fiber);
215+
});
198216
} else {
199217
if (!fiber.dom) fiber.dom = createNode(fiber);
200218

src/signals/signal.ts

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@ import {
22
addEffect,
33
addSignal,
44
cleanUp,
5+
cleanUpWFiber,
6+
getCurrentFC,
57
} from "../rendering/functionalComponents";
8+
import { Fiber } from "../types";
69
import { isPlainObject, isPrimitive } from "../utils/general";
710
import { addEffectCleanup, batchUpdate } from "./batch";
811

@@ -53,17 +56,32 @@ export function reactiveAttribute(fn: Function) {
5356
export function createEffect(fn: Function) {
5457
if (typeof fn !== "function")
5558
throw new Error("createEffect takes a effect function as the argument");
56-
currentEffect = fn;
5759

5860
addEffect(fn);
59-
const effectCleanup = fn();
61+
if (!getCurrentFC()) runEffect(fn);
62+
}
63+
64+
export function runEffect(effect: Function, fiber?: Fiber) {
65+
if (typeof effect !== "function") return;
66+
67+
currentEffect = effect;
68+
69+
const effectCleanup = effect();
6070

6171
if (currentEffect.__signals && typeof effectCleanup === "function")
6272
addEffectCleanup(effectCleanup);
6373

64-
if (!currentEffect.__signals) {
74+
if (
75+
!currentEffect.__signals &&
76+
effectCleanup &&
77+
typeof effectCleanup === "function"
78+
) {
6579
// which means this effect does not have any signals associated with so its just a cleanup function that we need to call when the component unmounts
66-
cleanUp(effectCleanup);
80+
if (!fiber) {
81+
cleanUp(effectCleanup);
82+
} else {
83+
cleanUpWFiber(effectCleanup, fiber);
84+
}
6785
}
6886

6987
currentEffect = null;
@@ -75,11 +93,13 @@ function computed<T extends NormalSignal | any[] | Record<any, any>>(
7593
if (typeof fn !== "function")
7694
throw new Error("computed takes a function as the argument");
7795

96+
let firstRun = getCurrentFC() !== null;
7897
currentEffect = () => {
79-
let newVal = fn();
80-
if (newVal !== signal.value) {
81-
signal.update(newVal);
98+
if (firstRun) {
99+
firstRun = false;
100+
return;
82101
}
102+
signal.update(fn());
83103
};
84104

85105
addEffect(currentEffect);

src/tests/rendering/extraRender.test.tsx

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -464,7 +464,6 @@ describe("Extra Edge cases", () => {
464464
const FC = () => {
465465
createEffect(() => {
466466
count.update((prev) => prev + 1);
467-
count.value;
468467
return () => {
469468
fn();
470469
};
@@ -479,11 +478,17 @@ describe("Extra Edge cases", () => {
479478
commitFiber(fiber);
480479

481480
expect(fn).toHaveBeenCalledTimes(0);
481+
await Promise.resolve();
482+
483+
expect(count.value).toBe(1);
484+
await Promise.resolve();
485+
482486
expect(fiber.dom.innerHTML).toBe("<div>1</div>");
483487

484488
visible.update(false);
485489
await Promise.resolve();
486490

491+
expect(visible.value).toBe(false);
487492
expect(fn).toHaveBeenCalledTimes(1);
488493
expect(fiber.dom.innerHTML).toBe("hidden");
489494
});
@@ -493,8 +498,8 @@ describe("Extra Edge cases", () => {
493498

494499
const FC = () => {
495500
createEffect(() => {
496-
count.update((prev) => prev + 1);
497501
count.value;
502+
count.update((prev) => prev + 1);
498503
});
499504

500505
return <div>{() => count.value}</div>;
@@ -509,6 +514,11 @@ describe("Extra Edge cases", () => {
509514
createFiber(fiber);
510515
commitFiber(fiber);
511516

517+
await Promise.resolve();
518+
expect(count.value).toBe(1);
519+
520+
await Promise.resolve();
521+
512522
expect(fiber.dom.innerHTML).toBe("<div>1</div>");
513523

514524
count.update((prev) => prev + 1);

0 commit comments

Comments
 (0)