Skip to content

Commit

Permalink
Introduce flushBatch()
Browse files Browse the repository at this point in the history
  • Loading branch information
yishn committed May 10, 2024
1 parent fdcf4aa commit 183e367
Show file tree
Hide file tree
Showing 5 changed files with 79 additions and 29 deletions.
2 changes: 1 addition & 1 deletion src/intrinsic/If.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ test("If", async () => {
<h1>Success!</h1>
</If>
<ElseIf condition={() => obj() != null}>
<h1>{() => obj()!.value}</h1>
<h1>{() => obj()?.value}</h1>
</ElseIf>
<Else>
<h1>{failMessage}</h1>
Expand Down
5 changes: 3 additions & 2 deletions src/mod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,12 @@ export {
type SubscopeOptions,
type RefSignal,
type RefSignalSetter,
useBatch,
useSubscope,
useMemo,
useSignal,
useRef,
useSubscope,
useBatch,
flushBatch,
} from "./scope.js";

export * from "./intrinsic/mod.js";
Expand Down
22 changes: 22 additions & 0 deletions src/scope.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,3 +98,25 @@ test("Manually track effect dependencies", () => {

destroy2();
});

test("Manually track effect dependencies with useMemo", () => {
const [, destroy] = useSubscope(() => {
const [a, setA] = useSignal(1);
const [b, setB] = useSignal(1);
const bMemo = useMemo(() => b());
let triggerCount = 0;

useEffect(() => {
triggerCount++;
}, [bMemo, a]);

useBatch(() => {
setA((n) => n + 1);
setB((n) => n + 1);
});

assert.strictEqual(triggerCount, 2);
});

destroy();
});
68 changes: 47 additions & 21 deletions src/scope.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ const createScope = (parent?: Scope): Scope => {
for (let i = this._effects.length - 1; i >= 0; i--) {
const effect = this._effects[i];
effect._clean?.();
effect._run = () => {}
effect._run = () => {};

effect._deps.forEach((signal) => signal._effects.delete(effect));
effect._deps.clear();
Expand All @@ -113,7 +113,7 @@ let currUntracked: boolean = false;
let currEffect: Effect | undefined;
let currBatch:
| {
_setters: (() => void)[];
_setters: (() => Signal<unknown>)[];
_effects: Set<Effect>;
}
| undefined;
Expand Down Expand Up @@ -161,7 +161,10 @@ export const useSignal: (<T>(
if (innerOpts?.force) {
value = newValue;
} else {
currBatch._setters.push(() => (value = newValue));
currBatch._setters.push(() => {
value = newValue;
return signal;
});
}

if (!innerOpts?.silent) {
Expand All @@ -183,35 +186,58 @@ export const useSignal: (<T>(
* and updated at the same time.
*/
export const useBatch = <T>(fn: () => T): T => {
const prevBatch = currBatch;
currBatch = { _setters: [], _effects: new Set() };
const createBatch = !currBatch;
if (createBatch) currBatch = { _setters: [], _effects: new Set() };

try {
const result = fn();
const result = fn();

while (currBatch._setters.length > 0 || currBatch._effects.size > 0) {
// Clean effect subscope
if (createBatch) {
flushBatch();
currBatch = undefined;
}

return result;
};

const effects = currBatch._effects;
currBatch._effects = new Set();
export function flushBatch(): void {
const mutatedSignals = new Set<Signal<unknown>>();

effects.forEach((effect) => effect._clean?.());
while (
currBatch &&
(currBatch._setters.length > 0 || currBatch._effects.size > 0)
) {
// Clean effect subscope

// Run signal updates
const effects = currBatch._effects;
currBatch._effects = new Set();

currBatch._setters.forEach((setter) => setter());
currBatch._setters = [];
effects.forEach((effect) => effect._clean?.());

// Run effects
// Run signal updates

effects.forEach((effect) => effect._run());
const settersCount = currBatch._setters.length;

for (const setter of currBatch._setters) {
const signal = setter();
mutatedSignals.add(signal);
}

return result;
} finally {
currBatch = prevBatch;
currBatch._setters = [];

// Run effects

for (const effect of effects) {
if (
!settersCount ||
[...effect._deps].every((dep) => mutatedSignals.has(dep))
) {
effect._run();
} else {
currBatch._effects.add(effect);
}
}
}
};
}

/**
* Creates an effect which will rerun when any accessed signal changes.
Expand Down
11 changes: 6 additions & 5 deletions web/docs/reactivity/effects.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,14 +112,15 @@ performance, but also to ensure that all signals are in a consistent state when
the effect is executed.

To manually ensure the effects are executed immediately inside an effect or
event listener, you can also use `useBatch`:
event listener, you can also use `flushBatch`:

```ts
import { useEffect, flushBatch } from "sinho";

useEffect(() => {
useBatch(() => {
setName("Charlie");
setGender("nonbinary");
}); // When `useBatch` is called, effect executions will be triggered
setName("Charlie");
setGender("nonbinary");
flushBatch();

console.log(name(), gender()); // Prints "Charlie nonbinary"
});
Expand Down

0 comments on commit 183e367

Please sign in to comment.