Skip to content

Allow Customizing Diff Logic (via new shouldUpdate method) #218

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -51,6 +51,7 @@
"esbuild": "^0.14.54",
"eslint": "^8.21.0",
"eslint-config-prettier": "^8.5.0",
"fast-deep-equal": "^3.1.3",
"husky": "^8.0.1",
"karma": "6.3.16",
"karma-chai-sinon": "^0.1.5",
11 changes: 10 additions & 1 deletion packages/core/src/index.ts
Original file line number Diff line number Diff line change
@@ -206,6 +206,9 @@ declare class Signal<T = any> {

peek(): T;

/** if true, triggers an update with the new incoming value */
shouldUpdate(oldValue: T, newValue: T): boolean;

get value(): T;
set value(value: T);
}
@@ -280,6 +283,10 @@ Signal.prototype.peek = function () {
return this._value;
};

Signal.prototype.shouldUpdate = (oldValue, newValue) => {
return newValue !== oldValue;
};

Object.defineProperty(Signal.prototype, "value", {
get() {
const node = addDependency(this);
@@ -289,7 +296,9 @@ Object.defineProperty(Signal.prototype, "value", {
return this._value;
},
set(value) {
if (value !== this._value) {
const shouldUpdate = this.shouldUpdate(this._value, value);

if (shouldUpdate) {
if (batchIteration > 100) {
cycleDetected();
}
44 changes: 44 additions & 0 deletions packages/core/test/signal.test.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { signal, computed, effect, batch, Signal } from "@preact/signals-core";
import fastEqual from "fast-deep-equal/es6";

describe("signal", () => {
it("should return value", () => {
@@ -1711,4 +1712,47 @@ describe("batch/transaction", () => {
});
expect(callCount).to.equal(1);
});

it("allows customizing the default shouldUpdate compare method", () => {
const mapA = new Map<string, string>();
const a = signal(mapA);
const spy1 = sinon.spy(() => a.value);
effect(spy1);

// before: re-renders every time, even if the value doesn't change
a.value = new Map(a.peek()).set("foo", "bar");
a.value = new Map(a.peek()).set("foo", "baz");
a.value = new Map(a.peek()).set("foo", "baz");
a.value = new Map(a.peek()).set("foo", "baz");

// should have been called twice but instead re-renders regardless on value pass in (without specifying a custom shouldUpdate method)
expect(spy1.callCount).to.equal(5);

Signal.prototype.shouldUpdate = (oldValue, newValue) => {
if (oldValue instanceof Map && newValue instanceof Map) {
return fastEqual(oldValue, newValue) === false;
}
return oldValue !== newValue;
};

function signal2<T>(value: T): Signal<T> {
return new Signal(value);
}

const mapB = new Map<string, string>();
const b = signal2(mapB);
const spy2 = sinon.spy(() => b.value);
effect(spy2);

// after: only re-renders if the value changes (via custom shouldUpdate method)
b.value = new Map(b.peek()).set("foo", "bar");
b.value = new Map(b.peek()).set("foo", "baz");
b.value = new Map(b.peek()).set("foo", "baz");
b.value = new Map(b.peek()).set("foo", "baz");

// setting up the initial empty Map --> update #1
// adding foo, bar to empty map --> update #2
// updating foo, bar to foo, baz --> update #3
expect(spy2.callCount).to.equal(3);
});
});
2 changes: 2 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.