Skip to content

Commit 862d9d6

Browse files
eddywJoviDeCroock
andauthored
Disallow side-effects in computed (#231)
* add test case for computed with side-effects * disallow side-effects within computed * add changeset * use instanceof instead of bit flags * update test in accessing computed within effects --------- Co-authored-by: Jovi De Croock <[email protected]>
1 parent 0135d60 commit 862d9d6

File tree

3 files changed

+23
-14
lines changed

3 files changed

+23
-14
lines changed

.changeset/happy-pants-rhyme.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@preact/signals-core": minor
3+
---
4+
5+
Disallow side-effects in computed

packages/core/src/index.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
function cycleDetected(): never {
22
throw new Error("Cycle detected");
33
}
4+
function mutationDetected(): never {
5+
throw new Error("Computed cannot have side-effects");
6+
}
47

58
// Flags for Computed and Effect.
69
const RUNNING = 1 << 0;
@@ -300,7 +303,11 @@ Object.defineProperty(Signal.prototype, "value", {
300303
}
301304
return this._value;
302305
},
303-
set(value) {
306+
set(this: Signal, value) {
307+
if (evalContext instanceof Computed) {
308+
mutationDetected();
309+
}
310+
304311
if (value !== this._value) {
305312
if (batchIteration > 100) {
306313
cycleDetected();
@@ -738,4 +745,4 @@ function effect(compute: () => void | (() => void)): () => void {
738745
return effect._dispose.bind(effect);
739746
}
740747

741-
export { signal, computed, effect, batch, Signal, ReadonlySignal };
748+
export { signal, computed, effect, batch, Signal, type ReadonlySignal };

packages/core/test/signal.test.tsx

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -559,7 +559,7 @@ describe("effect()", () => {
559559
expect(fn).to.throw(/Cycle detected/);
560560
});
561561

562-
it("should throw on indirect cycles", () => {
562+
it("should throw if a computed tries to set a signal's value", () => {
563563
const a = signal(0);
564564
let i = 0;
565565

@@ -578,7 +578,7 @@ describe("effect()", () => {
578578
c.value;
579579
});
580580

581-
expect(fn).to.throw(/Cycle detected/);
581+
expect(fn).to.throw(/Computed cannot have side-effects/);
582582
});
583583

584584
it("should allow disposing the effect multiple times", () => {
@@ -862,16 +862,13 @@ describe("computed()", () => {
862862
expect(spy).to.be.calledOnce;
863863
});
864864

865-
it("should recompute if a dependency changes during computation after becoming a dependency", () => {
866-
const a = signal(0);
867-
const spy = sinon.spy(() => {
868-
a.value++;
869-
});
870-
const c = computed(spy);
871-
c.value;
872-
expect(spy).to.be.calledOnce;
873-
c.value;
874-
expect(spy).to.be.calledTwice;
865+
it("should disallow setting signal's value", () => {
866+
const v: number = 123;
867+
const a: Signal = signal(v);
868+
const c: Signal = computed(() => a.value++);
869+
870+
expect(() => c.value).to.throw(/Computed cannot have side-effects/);
871+
expect(a.value).to.equal(v);
875872
});
876873

877874
it("should detect simple dependency cycles", () => {

0 commit comments

Comments
 (0)