Skip to content
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

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

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
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
Expand Up @@ -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",
Expand Down
11 changes: 10 additions & 1 deletion packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down Expand Up @@ -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);
Expand All @@ -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();
}
Expand Down
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", () => {
Expand Down Expand Up @@ -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.