Skip to content

Commit 2383684

Browse files
authored
Correctly peek and replace value (#76)
1 parent b8740e3 commit 2383684

File tree

5 files changed

+105
-7
lines changed

5 files changed

+105
-7
lines changed

.changeset/nice-timers-sort.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@preact/signals": patch
3+
---
4+
5+
Correctly replace props-value with peeked value

packages/core/src/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,6 @@ export class Signal<T = any> {
9191
if (this._value !== value) {
9292
this._value = value;
9393
let isFirst = pending.size === 0;
94-
9594
pending.add(this);
9695
// in batch mode this signal may be marked already
9796
if (this._pending === 0) {

packages/preact/src/index.ts

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -58,14 +58,14 @@ function createUpdater(updater: () => void) {
5858
function getElementUpdater(vnode: VNode) {
5959
let updater = updaterForComponent.get(vnode) as ElementUpdater;
6060
if (!updater) {
61-
let signalProps: string[] = [];
61+
let signalProps: Array<{ _key: string, _signal: Signal }> = [];
6262
updater = createUpdater(() => {
6363
let dom = vnode.__e as Element;
64-
let props = vnode.props;
6564

6665
for (let i = 0; i < signalProps.length; i++) {
67-
let prop = signalProps[i];
68-
let value = props[prop]._value;
66+
let { _key: prop, _signal: signal } = signalProps[i];
67+
let value = signal._value;
68+
if (!dom) return;
6969
if (prop in dom) {
7070
// @ts-ignore-next-line silly
7171
dom[prop] = value;
@@ -152,7 +152,18 @@ hook(OptionsTypes.DIFF, (old, vnode) => {
152152
// first Signal prop triggers creation/cleanup of the updater:
153153
if (!updater) updater = getElementUpdater(vnode);
154154
// track which props are Signals for precise updates:
155-
updater._props.push(i);
155+
updater._props.push({ _key: i, _signal: value });
156+
let newUpdater = updater._updater
157+
if (value._updater) {
158+
let oldUpdater = value._updater
159+
value._updater = () => {
160+
newUpdater();
161+
oldUpdater();
162+
}
163+
} else {
164+
value._updater = newUpdater
165+
}
166+
props[i] = value.peek()
156167
}
157168
}
158169

packages/preact/src/internal.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export interface ComponentType extends Component {
1818
export type Updater = Signal<unknown>;
1919

2020
export interface ElementUpdater extends Updater {
21-
_props: string[];
21+
_props: Array<{ _key: string, _signal: Signal }>;
2222
}
2323

2424
export const enum OptionsTypes {

packages/preact/test/index.test.ts

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import { h, render } from "preact";
33
import { useMemo } from "preact/hooks";
44
import { setupRerender } from "preact/test-utils";
55

6+
const sleep = (ms?: number) => new Promise(r => setTimeout(r, ms));
7+
68
describe("@preact/signals", () => {
79
let scratch: HTMLDivElement;
810
let rerender: () => void;
@@ -146,4 +148,85 @@ describe("@preact/signals", () => {
146148
expect(scratch.textContent).to.equal("bar");
147149
});
148150
});
151+
152+
describe("prop bindings", () => {
153+
it("should set the initial value of the checked property", () => {
154+
const s = signal(true);
155+
// @ts-ignore
156+
render(h("input", { checked: s }), scratch);
157+
158+
expect(scratch.firstChild).to.have.property("checked", true);
159+
expect(s.value).to.equal(true);
160+
});
161+
162+
it("should update the checked property on change", () => {
163+
const s = signal(true);
164+
// @ts-ignore
165+
render(h("input", { checked: s }), scratch);
166+
167+
expect(scratch.firstChild).to.have.property("checked", true);
168+
169+
s.value = false;
170+
171+
expect(scratch.firstChild).to.have.property("checked", false);
172+
});
173+
174+
it("should update props without re-rendering", async () => {
175+
const s = signal("initial");
176+
const spy = sinon.spy();
177+
function Wrap() {
178+
spy();
179+
// @ts-ignore
180+
return h("input", { value: s });
181+
}
182+
render(h(Wrap, {}), scratch);
183+
spy.resetHistory();
184+
185+
expect(scratch.firstChild).to.have.property("value", "initial");
186+
187+
s.value = "updated";
188+
189+
expect(scratch.firstChild).to.have.property("value", "updated");
190+
191+
// ensure the component was never re-rendered: (even after a tick)
192+
await sleep();
193+
expect(spy).not.to.have.been.called;
194+
195+
s.value = "second update";
196+
197+
expect(scratch.firstChild).to.have.property("value", "second update");
198+
199+
// ensure the component was never re-rendered: (even after a tick)
200+
await sleep();
201+
expect(spy).not.to.have.been.called;
202+
});
203+
204+
it("should set and update string style property", async () => {
205+
const style = signal("left: 10px");
206+
const spy = sinon.spy();
207+
function Wrap() {
208+
spy();
209+
// @ts-ignore
210+
return h("div", { style });
211+
}
212+
render(h(Wrap, {}), scratch);
213+
spy.resetHistory();
214+
215+
const div = scratch.firstChild as HTMLDivElement;
216+
217+
expect(div.style).to.have.property("left", "10px");
218+
219+
// ensure the component was never re-rendered: (even after a tick)
220+
await sleep();
221+
expect(spy).not.to.have.been.called;
222+
223+
style.value = "left: 20px;";
224+
225+
expect(div.style).to.have.property("left", "20px");
226+
227+
// ensure the component was never re-rendered: (even after a tick)
228+
await sleep();
229+
expect(spy).not.to.have.been.called;
230+
});
231+
});
149232
});

0 commit comments

Comments
 (0)