diff --git a/README.md b/README.md index af80bb7..bda166d 100644 --- a/README.md +++ b/README.md @@ -96,6 +96,47 @@ stopScope(); count(3); // No console output ``` +#### Manual Triggering + +The `trigger()` function allows you to manually trigger updates for downstream dependencies when you've directly mutated a signal's value without using the signal setter: + +```ts +import { signal, computed, trigger } from 'alien-signals'; + +const arr = signal([]); +const length = computed(() => arr().length); + +console.log(length()); // 0 + +// Direct mutation doesn't automatically trigger updates +arr().push(1); +console.log(length()); // Still 0 + +// Manually trigger updates +trigger(arr); +console.log(length()); // 1 +``` + +You can also trigger multiple signals at once: + +```ts +import { signal, computed, trigger } from 'alien-signals'; + +const src1 = signal([]); +const src2 = signal([]); +const total = computed(() => src1().length + src2().length); + +src1().push(1); +src2().push(2); + +trigger(() => { + src1(); + src2(); +}); + +console.log(total()); // 2 +``` + #### Creating Your Own Surface API You can reuse alien-signals’ core algorithm via `createReactiveSystem()` to build your own signal API. For implementation examples, see: diff --git a/src/index.ts b/src/index.ts index 9747f86..38ff234 100644 --- a/src/index.ts +++ b/src/index.ts @@ -182,6 +182,32 @@ export function effectScope(fn: () => void): () => void { return effectScopeOper.bind(e); } +export function trigger(fn: () => void) { + const sub: ReactiveNode = { + deps: undefined, + depsTail: undefined, + flags: ReactiveFlags.Watching, + }; + const prevSub = setActiveSub(sub); + try { + fn(); + } finally { + setActiveSub(prevSub); + do { + const link = sub.deps!; + const dep = link.dep; + unlink(link, sub); + if (dep.subs !== undefined) { + propagate(dep.subs); + shallowPropagate(dep.subs); + } + } while (sub.deps !== undefined); + if (!batchDepth) { + flush(); + } + } +} + function updateComputed(c: Computed): boolean { ++cycle; c.depsTail = undefined; diff --git a/tests/trigger.spec.ts b/tests/trigger.spec.ts new file mode 100644 index 0000000..3646b49 --- /dev/null +++ b/tests/trigger.spec.ts @@ -0,0 +1,46 @@ +import { expect, test } from 'vitest'; +import { computed, effect, trigger, signal } from '../src'; + +test('should trigger updates for dependent computed signals', () => { + const arr = signal([]); + const length = computed(() => arr().length); + + expect(length()).toBe(0); + arr().push(1); + trigger(arr); + expect(length()).toBe(1); +}); + +test('should trigger updates for the second source signal', () => { + const src1 = signal([]); + const src2 = signal([]); + const length = computed(() => src2().length); + + expect(length()).toBe(0); + src2().push(1); + trigger(() => { + src1(); + src2(); + }); + expect(length()).toBe(1); +}); + +test('should trigger effect once', () => { + const src1 = signal([]); + const src2 = signal([]); + + let triggers = 0; + + effect(() => { + triggers++; + src1(); + src2(); + }); + + expect(triggers).toBe(1); + trigger(() => { + src1(); + src2(); + }); + expect(triggers).toBe(2); +});