Skip to content

Commit

Permalink
Address review comments
Browse files Browse the repository at this point in the history
  • Loading branch information
justinfagnani committed May 7, 2024
1 parent 4e63a01 commit 3349ce5
Show file tree
Hide file tree
Showing 4 changed files with 70 additions and 43 deletions.
6 changes: 0 additions & 6 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,5 @@
# Changelog

## Unreleased

#### :rocket: Enhancement
* `signal-utils`
* [#61](https://github.com/proposal-signals/signal-utils/pull/61) Add `reaction()` function to run effects when watched values change.

## Release (2024-04-22)

signal-utils 0.15.0 (minor)
Expand Down
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ npm add signal-utils signal-polyfill
- [@deepSignal](#deepSignal)
- subtle utilities
- [effect](#leaky-effect-via-queuemicrotask)
- [reaction](#reaction)

### `@signal`

Expand Down Expand Up @@ -485,6 +486,27 @@ count.set(1);
// => 1 logs
```
#### Reactions
A reaction tracks a computation and calls an effect function when the value of
the computation changes.
```js
import { Signal } from 'signal-polyfill';
import { reaction } from 'signal-utils/subtle/reaction.js';

const a = new Signal.State(0);
const b = new Signal.State(1);

reaction(
() => a.get() + b.get(),
(value, previousValue) => console.log(value, previousValue)
);

a.set(1);
// after a microtask, logs: 2, 1
```
## Contributing
Expand Down
3 changes: 2 additions & 1 deletion src/subtle/reaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,15 @@ import { Signal } from "signal-polyfill";
export const reaction = <T>(
data: () => T,
effect: (value: T, previousValue: T) => void,
equals = Object.is
equals = Object.is,
) => {
// Passing equals here doesn't seem to dedupe the effect calls.
const computed: Signal.Computed<T> | undefined = new Signal.Computed(data, {
equals,
});
let previousValue = computed.get();
let notify: (() => Promise<void>) | undefined = async () => {
// await 0 is a cheap way to queue a microtask
await 0;
// Check if this reaction was unsubscribed
if (notify === undefined) {
Expand Down
82 changes: 46 additions & 36 deletions tests/subtle/reaction.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,30 @@ import { Signal } from "signal-polyfill";
import { reaction } from "../../src/subtle/reaction.ts";

describe("reaction()", () => {

test("calls the effect function when the data function return value changes", async () => {
const count = new Signal.State(0);

let callCount = 0;
let value, previousValue;

reaction(() => count.get(), (_value, _previousValue) => {
callCount++;
value = _value;
previousValue = _previousValue;
});
reaction(
() => count.get(),
(_value, _previousValue) => {
callCount++;
value = _value;
previousValue = _previousValue;
},
);

// Effect callbacks are not called immediately
assert.strictEqual(callCount, 0);

await 0;

// Effect callbacks are not called until the data function changes
assert.strictEqual(callCount, 0);

// Effect callbacks are called when the data function changes
// Effect callbacks are called when the data function changes
count.set(count.get() + 1);
await 0;
assert.strictEqual(callCount, 1);
Expand All @@ -45,15 +47,18 @@ describe("reaction()", () => {
const count = new Signal.State(0);

let callCount = 0;
const unsubscribe = reaction(() => count.get(), () => {
callCount++;
});
const unsubscribe = reaction(
() => count.get(),
() => {
callCount++;
},
);

// Check reaction is live
count.set(count.get() + 1);
await 0;
assert.strictEqual(callCount, 1);

unsubscribe();

// Check reaction is not live
Expand All @@ -66,15 +71,18 @@ describe("reaction()", () => {
const count = new Signal.State(0);

let callCount = 0;
const unsubscribe = reaction(() => count.get(), () => {
callCount++;
});
const unsubscribe = reaction(
() => count.get(),
() => {
callCount++;
},
);

// Check reaction is live
count.set(count.get() + 1);
await 0;
assert.strictEqual(callCount, 1);

// Check reaction is not live
count.set(count.get() + 1);
unsubscribe();
Expand All @@ -87,13 +95,13 @@ describe("reaction()", () => {
const b = new Signal.State(0);

let callCount = 0;
let value, previousValue;

reaction(() => a.get() + b.get(), (_value, _previousValue) => {
callCount++;
value = _value;
previousValue = _previousValue;
});
reaction(
() => a.get() + b.get(),
(_value, _previousValue) => {
callCount++;
},
);

// 1 + -1 still equals 0
a.set(1);
Expand All @@ -109,22 +117,25 @@ describe("reaction()", () => {
let value, previousValue;
let thrown = false;

if (typeof process !== 'undefined') {
process.on('uncaughtException', (error) => {
console.log('uncaughtException', error);
if (typeof process !== "undefined") {
process.on("uncaughtException", (error) => {
console.log("uncaughtException", error);
});
}

reaction(() => x.get(), (_value, _previousValue) => {
callCount++;
value = _value;
previousValue = _previousValue;
if (value === 1) {
thrown = true;
throw new Error("Oops");
}
thrown = false;
});
reaction(
() => x.get(),
(_value, _previousValue) => {
callCount++;
value = _value;
previousValue = _previousValue;
if (value === 1) {
thrown = true;
throw new Error("Oops");
}
thrown = false;
},
);

x.set(1);
await 0;
Expand All @@ -138,5 +149,4 @@ describe("reaction()", () => {
assert.strictEqual(value, 2);
assert.strictEqual(previousValue, 1);
});

});

0 comments on commit 3349ce5

Please sign in to comment.